feat: complete Epic 1 production foundation
This commit is contained in:
+110
-32
@@ -11,6 +11,8 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
},
|
||||
loading: false,
|
||||
loadError: '',
|
||||
usageEpoch: 0,
|
||||
exporting: false,
|
||||
};
|
||||
|
||||
var periodSelect = document.getElementById('period');
|
||||
@@ -20,6 +22,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
var cardList = document.getElementById('usage-card-list');
|
||||
var chartTemplate = document.getElementById('tmpl-chart-bar');
|
||||
var rowTemplate = document.getElementById('tmpl-usage-row');
|
||||
var outcomeList = document.getElementById('usage-outcome-list');
|
||||
var subtitle = document.querySelector('.section-card-subtitle');
|
||||
var statCards = document.querySelectorAll('.stats-grid .stat-card');
|
||||
|
||||
@@ -73,6 +76,12 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
return 'REST';
|
||||
}
|
||||
|
||||
function outcomeLabel(outcome) {
|
||||
var key = 'usage.outcome.' + outcome;
|
||||
var translated = tKey(key);
|
||||
return translated === key ? outcome : translated;
|
||||
}
|
||||
|
||||
function localizedUsageOperation(operation) {
|
||||
if (!window.localizeDemoOperation) {
|
||||
return operation;
|
||||
@@ -439,6 +448,43 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
return item;
|
||||
}
|
||||
|
||||
function renderOutcomes() {
|
||||
if (!outcomeList) {
|
||||
return;
|
||||
}
|
||||
outcomeList.innerHTML = '';
|
||||
var outcomes = state.usage ? (state.usage.outcomes || []) : [];
|
||||
if (!outcomes.length) {
|
||||
outcomeList.appendChild(buildEmptyState(
|
||||
tKey('usage.outcomes.empty.title'),
|
||||
tKey('usage.outcomes.empty.sub'),
|
||||
true
|
||||
));
|
||||
return;
|
||||
}
|
||||
outcomes.forEach(function (outcome) {
|
||||
var card = element('div', 'resource-card');
|
||||
var header = element('div', 'resource-card-header');
|
||||
var headerMain = element('div');
|
||||
headerMain.appendChild(element('div', 'resource-card-title', outcomeLabel(outcome.group)));
|
||||
headerMain.appendChild(element(
|
||||
'div',
|
||||
'resource-card-subtitle',
|
||||
outcome.execution_error_code || tKey('usage.outcome.no_error_code')
|
||||
));
|
||||
header.appendChild(headerMain);
|
||||
header.appendChild(element('span', 'badge', formatCount(outcome.calls_total)));
|
||||
card.appendChild(header);
|
||||
var metaGrid = element('div', 'resource-meta-grid');
|
||||
metaGrid.appendChild(buildUsageMetaItem('usage.table.th.calls', formatCount(outcome.calls_total)));
|
||||
metaGrid.appendChild(buildUsageMetaItem('usage.outcomes.p50', formatMs(outcome.p50_ms)));
|
||||
metaGrid.appendChild(buildUsageMetaItem('usage.outcomes.p95', formatMs(outcome.p95_ms)));
|
||||
metaGrid.appendChild(buildUsageMetaItem('usage.outcomes.p99', formatMs(outcome.p99_ms)));
|
||||
card.appendChild(metaGrid);
|
||||
outcomeList.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function renderUsage() {
|
||||
if (state.loading && !state.usage) {
|
||||
renderEmpty(tKey('usage.loading.title'), tKey('usage.loading.sub'));
|
||||
@@ -452,6 +498,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
|
||||
renderStats();
|
||||
renderChart();
|
||||
renderOutcomes();
|
||||
renderTable();
|
||||
if (subtitle) {
|
||||
subtitle.textContent = tfKey('usage.table.subtitle', { period: periodLabel(state.period) });
|
||||
@@ -472,24 +519,46 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
return;
|
||||
}
|
||||
|
||||
var workspaceId = state.workspaceId;
|
||||
var epoch = ++state.usageEpoch;
|
||||
state.loading = true;
|
||||
state.loadError = '';
|
||||
renderUsage();
|
||||
|
||||
try {
|
||||
state.usage = await window.CrankApi.getUsageOverview(state.workspaceId, { period: state.period });
|
||||
var usage = await window.CrankApi.getUsageOverview(workspaceId, { period: state.period });
|
||||
if (epoch !== state.usageEpoch || workspaceId !== currentWorkspaceId()) {
|
||||
return;
|
||||
}
|
||||
state.usage = usage;
|
||||
recomputeDerivedState();
|
||||
} catch (error) {
|
||||
if (epoch !== state.usageEpoch) {
|
||||
return;
|
||||
}
|
||||
state.loadError = error.message || tKey('usage.error.load');
|
||||
state.usage = null;
|
||||
recomputeDerivedState();
|
||||
} finally {
|
||||
state.loading = false;
|
||||
renderUsage();
|
||||
if (epoch === state.usageEpoch) {
|
||||
state.loading = false;
|
||||
renderUsage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function exportCsv() {
|
||||
function csvCell(value) {
|
||||
var text = String(value === null || value === undefined ? '' : value);
|
||||
if (/^[=+\-@\t\r\n]/.test(text)) {
|
||||
text = "'" + text;
|
||||
}
|
||||
return '"' + text.replace(/"/g, '""') + '"';
|
||||
}
|
||||
|
||||
async function exportCsv() {
|
||||
if (!window.CrankApi || state.exporting) {
|
||||
return;
|
||||
}
|
||||
if (!state.usage) {
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.info(tKey('usage.export.empty.body'), tKey('usage.export.empty.title'));
|
||||
@@ -497,33 +566,39 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
return;
|
||||
}
|
||||
|
||||
var rows = [tKey('usage.csv.header')];
|
||||
state.usage.operations.forEach(function (operation) {
|
||||
var localizedOperation = localizedUsageOperation(operation);
|
||||
var errorRate = operation.calls_total === 0
|
||||
? 0
|
||||
: ((operation.calls_error / operation.calls_total) * 100);
|
||||
rows.push([
|
||||
'"' + (localizedOperation.operation_display_name || operation.operation_display_name) + '"',
|
||||
'"' + protocolLabel(operation.protocol) + '"',
|
||||
operation.calls_total,
|
||||
operation.calls_error,
|
||||
errorRate.toFixed(2),
|
||||
operation.p50_ms,
|
||||
operation.p95_ms,
|
||||
operation.p99_ms,
|
||||
].join(','));
|
||||
});
|
||||
|
||||
var blob = new Blob([rows.join('\r\n')], { type: 'text/csv' });
|
||||
var url = URL.createObjectURL(blob);
|
||||
var link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = 'crank-usage-' + state.period + '.csv';
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.success(tKey('usage.export.done.body'), tKey('usage.export.done.title'));
|
||||
var workspaceId = currentWorkspaceId();
|
||||
if (!workspaceId) {
|
||||
return;
|
||||
}
|
||||
var epoch = state.usageEpoch;
|
||||
state.exporting = true;
|
||||
if (exportBtn) {
|
||||
exportBtn.disabled = true;
|
||||
}
|
||||
try {
|
||||
var csv = await window.CrankApi.exportUsageCsv(workspaceId, { period: state.period });
|
||||
if (epoch !== state.usageEpoch || workspaceId !== currentWorkspaceId()) {
|
||||
return;
|
||||
}
|
||||
var blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
|
||||
var url = URL.createObjectURL(blob);
|
||||
var link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = 'crank-usage-' + state.period + '.csv';
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.success(tKey('usage.export.done.body'), tKey('usage.export.done.title'));
|
||||
}
|
||||
} catch (error) {
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.error(error.message || tKey('usage.export.error.body'), tKey('usage.export.error.title'));
|
||||
}
|
||||
} finally {
|
||||
state.exporting = false;
|
||||
if (exportBtn) {
|
||||
exportBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -539,7 +614,10 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
exportBtn.addEventListener('click', exportCsv);
|
||||
}
|
||||
|
||||
window.addEventListener('crank:workspacechange', loadUsage);
|
||||
window.addEventListener('crank:workspacechange', function () {
|
||||
state.usageEpoch += 1;
|
||||
loadUsage();
|
||||
});
|
||||
|
||||
if (window.whenWorkspacesReady) {
|
||||
window.whenWorkspacesReady().finally(loadUsage);
|
||||
|
||||
Reference in New Issue
Block a user