@littlebearapps/platform-admin-sdk 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -5
- package/dist/templates.d.ts +1 -1
- package/dist/templates.js +121 -3
- package/package.json +1 -1
- package/templates/full/dashboard/src/components/notifications/NotificationDropdown.tsx +130 -0
- package/templates/full/dashboard/src/components/notifications/NotificationItem.tsx +264 -0
- package/templates/full/dashboard/src/components/patterns/PatternInfoButton.tsx +60 -0
- package/templates/full/dashboard/src/components/reports/FeatureUsageReport.tsx +339 -0
- package/templates/full/dashboard/src/components/search/SearchResultGroup.tsx +46 -0
- package/templates/full/dashboard/src/components/search/SearchResultItem.tsx +212 -0
- package/templates/full/dashboard/src/pages/api/patterns/[id]/approve.ts +49 -0
- package/templates/full/dashboard/src/pages/api/patterns/[id]/reject.ts +50 -0
- package/templates/full/dashboard/src/pages/api/reports/digests/stats.ts +38 -0
- package/templates/full/dashboard/src/pages/api/reports/digests.ts +39 -0
- package/templates/full/dashboard/src/pages/api/search/reindex/[type].ts +56 -0
- package/templates/full/dashboard/src/pages/api/test-reports/[id].ts +102 -0
- package/templates/full/dashboard/src/pages/feedback.astro +365 -0
- package/templates/full/dashboard/src/pages/kiosk.astro +206 -0
- package/templates/full/dashboard/src/pages/map.astro +561 -0
- package/templates/full/dashboard/src/pages/revenue.astro +72 -0
- package/templates/full/dashboard/src/pages/tests.astro +431 -0
- package/templates/full/scripts/ops/audit-cost-anomaly.ts +430 -0
- package/templates/full/scripts/ops/verify-account-total.ts +256 -0
- package/templates/full/tests/integration/feedback-schema.test.ts +361 -0
- package/templates/full/tests/integration/r2-archive.test.ts +108 -0
- package/templates/shared/.github/workflows/dependabot-automerge.yml +41 -0
- package/templates/shared/.github/workflows/validate-controls.yml +27 -0
- package/templates/shared/dashboard/src/components/Breadcrumbs.astro +101 -0
- package/templates/shared/dashboard/src/components/EmptyState.astro +46 -0
- package/templates/shared/dashboard/src/components/ErrorBoundary.astro +79 -0
- package/templates/shared/dashboard/src/components/LoadingSkeleton.astro +105 -0
- package/templates/shared/dashboard/src/components/PageShell.astro +72 -0
- package/templates/shared/dashboard/src/components/SkipLinks.astro +22 -0
- package/templates/shared/dashboard/src/components/Toast.astro +170 -0
- package/templates/shared/dashboard/src/components/ToastContainer.astro +156 -0
- package/templates/shared/dashboard/src/components/costs/ProviderCostsGrid.tsx +401 -0
- package/templates/shared/dashboard/src/components/costs/index.ts +4 -0
- package/templates/shared/dashboard/src/components/overview/AlertBanner.tsx +94 -0
- package/templates/shared/dashboard/src/components/overview/index.ts +9 -0
- package/templates/shared/dashboard/src/components/resources/CostChart.tsx +170 -0
- package/templates/shared/dashboard/src/components/resources/ProviderCard.tsx +272 -0
- package/templates/shared/dashboard/src/components/resources/ProviderDetail.tsx +293 -0
- package/templates/shared/dashboard/src/components/settings/SettingsCard.astro +102 -0
- package/templates/shared/dashboard/src/components/usage/AllowanceGauge.astro +170 -0
- package/templates/shared/dashboard/src/components/usage/AnomalyAlerts.astro +633 -0
- package/templates/shared/dashboard/src/components/usage/BillingCycleCountdown.astro +192 -0
- package/templates/shared/dashboard/src/components/usage/BurnRateHero.astro +539 -0
- package/templates/shared/dashboard/src/components/usage/CircuitBreakerEventLog.astro +542 -0
- package/templates/shared/dashboard/src/components/usage/CircuitBreakerPanel.tsx +292 -0
- package/templates/shared/dashboard/src/components/usage/CircuitBreakerStatus.astro +669 -0
- package/templates/shared/dashboard/src/components/usage/CompactThresholdBanner.astro +531 -0
- package/templates/shared/dashboard/src/components/usage/ComparisonModeSelector.astro +651 -0
- package/templates/shared/dashboard/src/components/usage/CostBreakdownChart.astro +381 -0
- package/templates/shared/dashboard/src/components/usage/CostBreakdownTable.astro +210 -0
- package/templates/shared/dashboard/src/components/usage/CostDataTable.astro +0 -0
- package/templates/shared/dashboard/src/components/usage/CostDonutChart.astro +311 -0
- package/templates/shared/dashboard/src/components/usage/DailyCostChart.astro +632 -0
- package/templates/shared/dashboard/src/components/usage/ExportButton.astro +114 -0
- package/templates/shared/dashboard/src/components/usage/FeatureBudgetsTable.astro +872 -0
- package/templates/shared/dashboard/src/components/usage/FilterBar.astro +190 -0
- package/templates/shared/dashboard/src/components/usage/FilterToggles.astro +175 -0
- package/templates/shared/dashboard/src/components/usage/GitHubUsageCard.astro +537 -0
- package/templates/shared/dashboard/src/components/usage/OverageCostCard.astro +212 -0
- package/templates/shared/dashboard/src/components/usage/PlanUtilizationCard.astro +193 -0
- package/templates/shared/dashboard/src/components/usage/ProjectCard.astro +640 -0
- package/templates/shared/dashboard/src/components/usage/ProjectCardsGrid.astro +272 -0
- package/templates/shared/dashboard/src/components/usage/ResourceSearch.astro +279 -0
- package/templates/shared/dashboard/src/components/usage/ServiceUtilizationList.astro +604 -0
- package/templates/shared/dashboard/src/components/usage/SparklineCard.astro +399 -0
- package/templates/shared/dashboard/src/components/usage/StatsHero.astro +600 -0
- package/templates/shared/dashboard/src/components/usage/TableFilters.astro +1033 -0
- package/templates/shared/dashboard/src/components/usage/ThresholdAlert.astro +271 -0
- package/templates/shared/dashboard/src/components/usage/ThresholdSettings.astro +618 -0
- package/templates/shared/dashboard/src/components/usage/TopSpenderCard.astro +170 -0
- package/templates/shared/dashboard/src/components/usage/UnifiedResourceTable.astro +1737 -0
- package/templates/shared/dashboard/src/components/usage/UsageCard.astro +135 -0
- package/templates/shared/dashboard/src/components/usage/UsageHealthBanner.astro +387 -0
- package/templates/shared/dashboard/src/components/usage/UtilizationBar.astro +159 -0
- package/templates/shared/dashboard/src/components/usage/WorkersBreakdownTable.astro +659 -0
- package/templates/shared/dashboard/src/components/usage/daily/CostChart.astro +461 -0
- package/templates/shared/dashboard/src/components/usage/daily/CostTable.astro +946 -0
- package/templates/shared/dashboard/src/components/usage/daily/DailyOverview.astro +1079 -0
- package/templates/shared/dashboard/src/components/usage/design-tokens.ts +187 -0
- package/templates/shared/dashboard/src/components/usage/filters/InlineDateRange.astro +285 -0
- package/templates/shared/dashboard/src/components/usage/filters/PeriodButtons.astro +157 -0
- package/templates/shared/dashboard/src/components/usage/filters/ProjectSelect.astro +284 -0
- package/templates/shared/dashboard/src/components/usage/scripts/ai-tab-controller.ts +419 -0
- package/templates/shared/dashboard/src/components/usage/scripts/constants.ts +60 -0
- package/templates/shared/dashboard/src/components/usage/scripts/formatters.ts +62 -0
- package/templates/shared/dashboard/src/components/usage/scripts/overview-controller.ts +1633 -0
- package/templates/shared/dashboard/src/components/usage/scripts/resource-table-builder.ts +294 -0
- package/templates/shared/dashboard/src/components/usage/scripts/tabs-filters-controller.ts +464 -0
- package/templates/shared/dashboard/src/components/usage/state/index.ts +55 -0
- package/templates/shared/dashboard/src/components/usage/state/usageActions.ts +439 -0
- package/templates/shared/dashboard/src/components/usage/state/usageStore.ts +376 -0
- package/templates/shared/dashboard/src/components/usage/types.ts +283 -0
- package/templates/shared/dashboard/src/components/usage/usage-colors.ts +292 -0
- package/templates/shared/dashboard/src/pages/api/usage/ai-models.ts +235 -0
- package/templates/shared/dashboard/src/pages/api/usage/billing-context.ts +296 -0
- package/templates/shared/scripts/test-telemetry-flow.ts +464 -0
- package/templates/shared/tests/e2e/usage-export.test.ts +784 -0
- package/templates/shared/tests/e2e/usage-mobile.test.ts +531 -0
- package/templates/standard/dashboard/src/components/errors/PriorityBadge.astro +27 -0
- package/templates/standard/dashboard/src/components/infrastructure/HealthchecksStatus.tsx +293 -0
- package/templates/standard/dashboard/src/components/infrastructure/InfrastructureTabs.tsx +268 -0
- package/templates/standard/dashboard/src/pages/analytics.astro +64 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/alerts.ts +85 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks/[id]/flips.ts +110 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks.ts +101 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/uptime/[id]/response-times.ts +121 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/uptime.ts +89 -0
- package/templates/standard/dashboard/src/pages/api/test/service-auth.ts +178 -0
- package/templates/standard/tests/integration/connectors.test.ts +241 -0
- package/templates/standard/tests/integration/github-monitor.test.ts +143 -0
- package/templates/standard/tests/integration/ingestion.test.ts +211 -0
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tabs & Filters Controller
|
|
3
|
+
*
|
|
4
|
+
* Handles tab switching, filter state management, export functionality,
|
|
5
|
+
* and auto-refresh for the Usage Dashboard.
|
|
6
|
+
* Extracted from index.astro for task-22.5 (slim to <300 lines)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
renderResourceTable,
|
|
11
|
+
updateResourceCount,
|
|
12
|
+
type UnifiedResource,
|
|
13
|
+
} from './resource-table-builder';
|
|
14
|
+
import { getSSRConfig } from './constants';
|
|
15
|
+
|
|
16
|
+
// ========== Types ==========
|
|
17
|
+
|
|
18
|
+
export interface FilterState {
|
|
19
|
+
period: string;
|
|
20
|
+
project: string;
|
|
21
|
+
serviceTypes: string[];
|
|
22
|
+
searchQuery: string;
|
|
23
|
+
onlyChanged: boolean;
|
|
24
|
+
nonZeroCost: boolean;
|
|
25
|
+
compareMode: 'sequential' | 'same-period';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ========== State ==========
|
|
29
|
+
|
|
30
|
+
let allResources: UnifiedResource[] = [];
|
|
31
|
+
let filteredResources: UnifiedResource[] = [];
|
|
32
|
+
|
|
33
|
+
const filterState: FilterState = {
|
|
34
|
+
period: '30d',
|
|
35
|
+
project: 'all',
|
|
36
|
+
serviceTypes: [],
|
|
37
|
+
searchQuery: '',
|
|
38
|
+
onlyChanged: false,
|
|
39
|
+
nonZeroCost: false,
|
|
40
|
+
compareMode: 'sequential',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// ========== Tab Switching ==========
|
|
44
|
+
|
|
45
|
+
function initTabSwitching(): void {
|
|
46
|
+
const tabButtons = document.querySelectorAll('.tab-button');
|
|
47
|
+
const tabPanels = document.querySelectorAll('.tab-panel');
|
|
48
|
+
|
|
49
|
+
tabButtons.forEach((button) => {
|
|
50
|
+
button.addEventListener('click', () => {
|
|
51
|
+
const tabId = button.getAttribute('data-tab');
|
|
52
|
+
if (!tabId) return;
|
|
53
|
+
|
|
54
|
+
// Update active states (using Tailwind hidden class)
|
|
55
|
+
tabButtons.forEach((btn) => {
|
|
56
|
+
btn.classList.remove('active');
|
|
57
|
+
btn.removeAttribute('data-active');
|
|
58
|
+
});
|
|
59
|
+
tabPanels.forEach((panel) => {
|
|
60
|
+
panel.classList.add('hidden');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
button.classList.add('active');
|
|
64
|
+
button.setAttribute('data-active', '');
|
|
65
|
+
const targetPanel = document.getElementById(`tab-${tabId}`);
|
|
66
|
+
if (targetPanel) {
|
|
67
|
+
targetPanel.classList.remove('hidden');
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ========== Filter Event Listeners ==========
|
|
74
|
+
|
|
75
|
+
function initFilterListeners(): void {
|
|
76
|
+
// Period selector
|
|
77
|
+
const periodButtons = document.querySelectorAll('[data-period]');
|
|
78
|
+
periodButtons.forEach((btn) => {
|
|
79
|
+
btn.addEventListener('click', () => {
|
|
80
|
+
const period = btn.getAttribute('data-period');
|
|
81
|
+
if (period) {
|
|
82
|
+
filterState.period = period;
|
|
83
|
+
updateURL();
|
|
84
|
+
// nanostores will handle data refresh via store subscriptions
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Project filter
|
|
90
|
+
const projectSelect = document.getElementById('project-filter') as HTMLSelectElement;
|
|
91
|
+
if (projectSelect) {
|
|
92
|
+
projectSelect.addEventListener('change', () => {
|
|
93
|
+
filterState.project = projectSelect.value;
|
|
94
|
+
updateURL();
|
|
95
|
+
applyFiltersAndRender();
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Search filter
|
|
100
|
+
const searchInput = document.getElementById('resource-search') as HTMLInputElement;
|
|
101
|
+
if (searchInput) {
|
|
102
|
+
searchInput.addEventListener(
|
|
103
|
+
'input',
|
|
104
|
+
debounce(() => {
|
|
105
|
+
filterState.searchQuery = searchInput.value;
|
|
106
|
+
applyFiltersAndRender();
|
|
107
|
+
}, 300)
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Toggle filters (only changed, non-zero cost)
|
|
112
|
+
const onlyChangedToggle = document.getElementById('only-changed-toggle') as HTMLInputElement;
|
|
113
|
+
if (onlyChangedToggle) {
|
|
114
|
+
onlyChangedToggle.addEventListener('change', () => {
|
|
115
|
+
filterState.onlyChanged = onlyChangedToggle.checked;
|
|
116
|
+
applyFiltersAndRender();
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const nonZeroCostToggle = document.getElementById('non-zero-cost-toggle') as HTMLInputElement;
|
|
121
|
+
if (nonZeroCostToggle) {
|
|
122
|
+
nonZeroCostToggle.addEventListener('change', () => {
|
|
123
|
+
filterState.nonZeroCost = nonZeroCostToggle.checked;
|
|
124
|
+
applyFiltersAndRender();
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Compare mode selector
|
|
129
|
+
const compareModeSelect = document.getElementById('compare-mode') as HTMLSelectElement;
|
|
130
|
+
if (compareModeSelect) {
|
|
131
|
+
compareModeSelect.addEventListener('change', () => {
|
|
132
|
+
filterState.compareMode = compareModeSelect.value as 'sequential' | 'same-period';
|
|
133
|
+
updateURL();
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ========== URL Sync ==========
|
|
139
|
+
|
|
140
|
+
function updateURL(): void {
|
|
141
|
+
const params = new URLSearchParams(window.location.search);
|
|
142
|
+
params.set('period', filterState.period);
|
|
143
|
+
if (filterState.project !== 'all') {
|
|
144
|
+
params.set('project', filterState.project);
|
|
145
|
+
} else {
|
|
146
|
+
params.delete('project');
|
|
147
|
+
}
|
|
148
|
+
params.set('compare', filterState.compareMode);
|
|
149
|
+
|
|
150
|
+
const newURL = `${window.location.pathname}?${params.toString()}`;
|
|
151
|
+
window.history.replaceState({}, '', newURL);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ========== Filter & Render ==========
|
|
155
|
+
|
|
156
|
+
function applyFiltersAndRender(): void {
|
|
157
|
+
filteredResources = allResources.filter((resource) => {
|
|
158
|
+
// Project filter
|
|
159
|
+
if (
|
|
160
|
+
filterState.project &&
|
|
161
|
+
filterState.project !== 'all' &&
|
|
162
|
+
resource.project !== filterState.project
|
|
163
|
+
) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Search filter
|
|
168
|
+
if (filterState.searchQuery) {
|
|
169
|
+
const query = filterState.searchQuery.toLowerCase();
|
|
170
|
+
if (!resource.name.toLowerCase().includes(query)) {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Only changed filter (>5% change or NEW)
|
|
176
|
+
if (filterState.onlyChanged) {
|
|
177
|
+
if (resource.costDeltaPct === 'NEW') return true;
|
|
178
|
+
if (typeof resource.costDeltaPct !== 'number') return false;
|
|
179
|
+
if (Math.abs(resource.costDeltaPct) <= 5) return false;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Non-zero cost filter
|
|
183
|
+
if (filterState.nonZeroCost && resource.costCurrent === 0) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return true;
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
renderResourceTable(filteredResources);
|
|
191
|
+
updateResourceCount(filteredResources.length);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ========== Export Functionality (Task-17.15) ==========
|
|
195
|
+
|
|
196
|
+
function getExportFilename(ext: string): string {
|
|
197
|
+
const now = new Date();
|
|
198
|
+
const month = now.toLocaleString('en-AU', { month: 'short' }).toLowerCase();
|
|
199
|
+
const year = now.getFullYear();
|
|
200
|
+
const project = filterState.project === 'all' ? 'all-projects' : filterState.project;
|
|
201
|
+
// Format: usage-brandcopilot-jan2026.csv
|
|
202
|
+
return `usage-${project}-${month}${year}.${ext}`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function downloadFile(content: string, filename: string, mimeType: string): void {
|
|
206
|
+
const blob = new Blob([content], { type: mimeType });
|
|
207
|
+
const url = URL.createObjectURL(blob);
|
|
208
|
+
const link = document.createElement('a');
|
|
209
|
+
link.href = url;
|
|
210
|
+
link.download = filename;
|
|
211
|
+
document.body.appendChild(link);
|
|
212
|
+
link.click();
|
|
213
|
+
document.body.removeChild(link);
|
|
214
|
+
URL.revokeObjectURL(url);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function exportToCsv(): void {
|
|
218
|
+
if (filteredResources.length === 0) {
|
|
219
|
+
alert('No data to export');
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const headers = [
|
|
224
|
+
'Name',
|
|
225
|
+
'Type',
|
|
226
|
+
'Project',
|
|
227
|
+
'Usage',
|
|
228
|
+
'Unit',
|
|
229
|
+
'% of Limit',
|
|
230
|
+
'Current Cost ($)',
|
|
231
|
+
'Prior Cost ($)',
|
|
232
|
+
'Change (%)',
|
|
233
|
+
'Status',
|
|
234
|
+
];
|
|
235
|
+
const rows = filteredResources.map((r) => [
|
|
236
|
+
r.name,
|
|
237
|
+
r.type,
|
|
238
|
+
r.project,
|
|
239
|
+
r.usage.value.toString(),
|
|
240
|
+
r.usage.unit,
|
|
241
|
+
r.limitPct != null ? r.limitPct.toFixed(1) : '',
|
|
242
|
+
r.costCurrent.toFixed(4),
|
|
243
|
+
r.costPrior.toFixed(4),
|
|
244
|
+
r.costDeltaPct === 'NEW' ? 'NEW' : (r.costDeltaPct?.toFixed(1) ?? '0'),
|
|
245
|
+
r.status,
|
|
246
|
+
]);
|
|
247
|
+
|
|
248
|
+
const csvContent = [headers, ...rows]
|
|
249
|
+
.map((row) => row.map((cell) => `"${cell.toString().replace(/"/g, '""')}"`).join(','))
|
|
250
|
+
.join('\n');
|
|
251
|
+
|
|
252
|
+
downloadFile(csvContent, getExportFilename('csv'), 'text/csv;charset=utf-8;');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function exportToJson(): void {
|
|
256
|
+
if (filteredResources.length === 0) {
|
|
257
|
+
alert('No data to export');
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const exportData = {
|
|
262
|
+
exportedAt: new Date().toISOString(),
|
|
263
|
+
filters: {
|
|
264
|
+
period: filterState.period,
|
|
265
|
+
project: filterState.project,
|
|
266
|
+
serviceTypes: filterState.serviceTypes,
|
|
267
|
+
searchQuery: filterState.searchQuery,
|
|
268
|
+
onlyChanged: filterState.onlyChanged,
|
|
269
|
+
nonZeroCost: filterState.nonZeroCost,
|
|
270
|
+
compareMode: filterState.compareMode,
|
|
271
|
+
},
|
|
272
|
+
summary: {
|
|
273
|
+
totalResources: filteredResources.length,
|
|
274
|
+
totalCurrentCost: filteredResources.reduce((sum, r) => sum + r.costCurrent, 0),
|
|
275
|
+
totalPriorCost: filteredResources.reduce((sum, r) => sum + r.costPrior, 0),
|
|
276
|
+
resourcesByType: filteredResources.reduce(
|
|
277
|
+
(acc, r) => {
|
|
278
|
+
acc[r.type] = (acc[r.type] || 0) + 1;
|
|
279
|
+
return acc;
|
|
280
|
+
},
|
|
281
|
+
{} as Record<string, number>
|
|
282
|
+
),
|
|
283
|
+
},
|
|
284
|
+
resources: filteredResources.map((r) => ({
|
|
285
|
+
id: r.id,
|
|
286
|
+
name: r.name,
|
|
287
|
+
type: r.type,
|
|
288
|
+
project: r.project,
|
|
289
|
+
repoUrl: r.repoUrl,
|
|
290
|
+
usage: r.usage,
|
|
291
|
+
limitPct: r.limitPct,
|
|
292
|
+
costCurrent: r.costCurrent,
|
|
293
|
+
costPrior: r.costPrior,
|
|
294
|
+
costDelta: r.costDelta,
|
|
295
|
+
costDeltaPct: r.costDeltaPct,
|
|
296
|
+
status: r.status,
|
|
297
|
+
})),
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const jsonContent = JSON.stringify(exportData, null, 2);
|
|
301
|
+
downloadFile(jsonContent, getExportFilename('json'), 'application/json;charset=utf-8;');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function initExportButtons(): void {
|
|
305
|
+
const exportCsvBtn = document.getElementById('export-csv');
|
|
306
|
+
const exportJsonBtn = document.getElementById('export-json');
|
|
307
|
+
|
|
308
|
+
if (exportCsvBtn) {
|
|
309
|
+
exportCsvBtn.addEventListener('click', exportToCsv);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (exportJsonBtn) {
|
|
313
|
+
exportJsonBtn.addEventListener('click', exportToJson);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ========== Auto-Refresh Functionality (Task-17.17) ==========
|
|
318
|
+
|
|
319
|
+
const REFRESH_INTERVAL = 30; // seconds
|
|
320
|
+
let autoRefreshTimer: ReturnType<typeof setInterval> | null = null;
|
|
321
|
+
let countdownTimer: ReturnType<typeof setInterval> | null = null;
|
|
322
|
+
let countdownValue = REFRESH_INTERVAL;
|
|
323
|
+
|
|
324
|
+
function updateCountdown(): void {
|
|
325
|
+
const refreshCountdown = document.getElementById('refresh-countdown');
|
|
326
|
+
if (refreshCountdown) {
|
|
327
|
+
refreshCountdown.textContent = `${countdownValue}s`;
|
|
328
|
+
}
|
|
329
|
+
countdownValue--;
|
|
330
|
+
if (countdownValue < 0) {
|
|
331
|
+
countdownValue = REFRESH_INTERVAL;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function refreshData(): Promise<void> {
|
|
336
|
+
try {
|
|
337
|
+
const params = new URLSearchParams();
|
|
338
|
+
params.set('period', filterState.period);
|
|
339
|
+
if (filterState.project !== 'all') {
|
|
340
|
+
params.set('project', filterState.project);
|
|
341
|
+
}
|
|
342
|
+
params.set('compare', filterState.compareMode);
|
|
343
|
+
|
|
344
|
+
// Include credentials to pass Cloudflare Access JWT cookie
|
|
345
|
+
const response = await fetch(`/api/usage?${params.toString()}`, {
|
|
346
|
+
credentials: 'include',
|
|
347
|
+
});
|
|
348
|
+
if (!response.ok) {
|
|
349
|
+
console.error('Failed to refresh usage data');
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const data = await response.json();
|
|
354
|
+
if (data.resources && Array.isArray(data.resources)) {
|
|
355
|
+
allResources = data.resources;
|
|
356
|
+
applyFiltersAndRender();
|
|
357
|
+
}
|
|
358
|
+
} catch (error) {
|
|
359
|
+
console.error('Error refreshing data:', error);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function startAutoRefresh(): void {
|
|
364
|
+
if (autoRefreshTimer) return;
|
|
365
|
+
|
|
366
|
+
const refreshCountdown = document.getElementById('refresh-countdown');
|
|
367
|
+
countdownValue = REFRESH_INTERVAL;
|
|
368
|
+
if (refreshCountdown) {
|
|
369
|
+
refreshCountdown.classList.remove('hidden');
|
|
370
|
+
updateCountdown();
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
countdownTimer = setInterval(updateCountdown, 1000);
|
|
374
|
+
autoRefreshTimer = setInterval(async () => {
|
|
375
|
+
await refreshData();
|
|
376
|
+
countdownValue = REFRESH_INTERVAL;
|
|
377
|
+
}, REFRESH_INTERVAL * 1000);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function stopAutoRefresh(): void {
|
|
381
|
+
if (autoRefreshTimer) {
|
|
382
|
+
clearInterval(autoRefreshTimer);
|
|
383
|
+
autoRefreshTimer = null;
|
|
384
|
+
}
|
|
385
|
+
if (countdownTimer) {
|
|
386
|
+
clearInterval(countdownTimer);
|
|
387
|
+
countdownTimer = null;
|
|
388
|
+
}
|
|
389
|
+
const refreshCountdown = document.getElementById('refresh-countdown');
|
|
390
|
+
if (refreshCountdown) {
|
|
391
|
+
refreshCountdown.classList.add('hidden');
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function initAutoRefresh(): void {
|
|
396
|
+
const autoRefreshToggle = document.getElementById('auto-refresh-toggle') as HTMLInputElement;
|
|
397
|
+
if (!autoRefreshToggle) return;
|
|
398
|
+
|
|
399
|
+
// Restore from localStorage
|
|
400
|
+
const savedAutoRefresh = localStorage.getItem('usage-auto-refresh');
|
|
401
|
+
if (savedAutoRefresh === 'true') {
|
|
402
|
+
autoRefreshToggle.checked = true;
|
|
403
|
+
startAutoRefresh();
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
autoRefreshToggle.addEventListener('change', () => {
|
|
407
|
+
if (autoRefreshToggle.checked) {
|
|
408
|
+
startAutoRefresh();
|
|
409
|
+
localStorage.setItem('usage-auto-refresh', 'true');
|
|
410
|
+
} else {
|
|
411
|
+
stopAutoRefresh();
|
|
412
|
+
localStorage.setItem('usage-auto-refresh', 'false');
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
// Stop auto-refresh when page is hidden
|
|
417
|
+
document.addEventListener('visibilitychange', () => {
|
|
418
|
+
if (document.hidden && autoRefreshToggle.checked) {
|
|
419
|
+
stopAutoRefresh();
|
|
420
|
+
} else if (!document.hidden && autoRefreshToggle.checked) {
|
|
421
|
+
startAutoRefresh();
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// ========== Utility Functions ==========
|
|
427
|
+
|
|
428
|
+
function debounce<T extends (...args: unknown[]) => void>(fn: T, delay: number): T {
|
|
429
|
+
let timeoutId: ReturnType<typeof setTimeout>;
|
|
430
|
+
return ((...args: unknown[]) => {
|
|
431
|
+
clearTimeout(timeoutId);
|
|
432
|
+
timeoutId = setTimeout(() => fn(...args), delay);
|
|
433
|
+
}) as T;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// ========== Resource Management ==========
|
|
437
|
+
|
|
438
|
+
export function setResources(resources: UnifiedResource[]): void {
|
|
439
|
+
allResources = resources;
|
|
440
|
+
applyFiltersAndRender();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export function getFilterState(): FilterState {
|
|
444
|
+
return { ...filterState };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// ========== Main Initialization ==========
|
|
448
|
+
|
|
449
|
+
export function initTabsAndFilters(): void {
|
|
450
|
+
// Load initial config from SSR data attributes
|
|
451
|
+
const config = getSSRConfig();
|
|
452
|
+
filterState.period = config.period;
|
|
453
|
+
filterState.project = config.project;
|
|
454
|
+
|
|
455
|
+
// Initialize all subsystems
|
|
456
|
+
initTabSwitching();
|
|
457
|
+
initFilterListeners();
|
|
458
|
+
initExportButtons();
|
|
459
|
+
initAutoRefresh();
|
|
460
|
+
|
|
461
|
+
// Expose setResources globally for data loading
|
|
462
|
+
(window as unknown as { usagePageSetResources: typeof setResources }).usagePageSetResources =
|
|
463
|
+
setResources;
|
|
464
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usage Dashboard State
|
|
3
|
+
*
|
|
4
|
+
* Re-exports all store atoms, computed values, and actions.
|
|
5
|
+
* Part of task-19: Usage Dashboard Refactor Phase 1 - Foundation
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import {
|
|
10
|
+
* $period, $dailyData, $isLoading,
|
|
11
|
+
* fetchDailyData, syncFromURL, setPeriod
|
|
12
|
+
* } from './state';
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// Store atoms
|
|
17
|
+
export {
|
|
18
|
+
$period,
|
|
19
|
+
$project,
|
|
20
|
+
$customDateRange,
|
|
21
|
+
$timezone,
|
|
22
|
+
$dailyData,
|
|
23
|
+
$isLoading,
|
|
24
|
+
$selectedDate,
|
|
25
|
+
$totalCost,
|
|
26
|
+
$dateRangeDisplay,
|
|
27
|
+
$dayCount,
|
|
28
|
+
$averageDailyCost,
|
|
29
|
+
$resourceTotals,
|
|
30
|
+
type Period,
|
|
31
|
+
} from './usageStore';
|
|
32
|
+
|
|
33
|
+
// Actions
|
|
34
|
+
export {
|
|
35
|
+
// Melbourne formatters
|
|
36
|
+
formatDateMelbourne,
|
|
37
|
+
formatDateTimeMelbourne,
|
|
38
|
+
formatDateShort,
|
|
39
|
+
getMelbourneDateString,
|
|
40
|
+
// URL sync
|
|
41
|
+
syncFromURL,
|
|
42
|
+
syncToURL,
|
|
43
|
+
// Data fetching
|
|
44
|
+
fetchDailyData,
|
|
45
|
+
// Subscriptions
|
|
46
|
+
initSubscriptions,
|
|
47
|
+
destroySubscriptions,
|
|
48
|
+
// UI actions
|
|
49
|
+
setPeriod,
|
|
50
|
+
setProject,
|
|
51
|
+
setCustomDateRange,
|
|
52
|
+
selectDate,
|
|
53
|
+
clearSelection,
|
|
54
|
+
resetFilters,
|
|
55
|
+
} from './usageActions';
|