feat: polish alpine live page states

This commit is contained in:
a.tolmachev
2026-03-31 17:03:26 +03:00
parent 7aabf7077a
commit ffa9cc87eb
8 changed files with 130 additions and 19 deletions
+1
View File
@@ -13,6 +13,7 @@
<script src="js/config.js"></script>
<script src="js/i18n.js"></script>
<script src="js/api.js"></script>
<script src="js/ui-feedback.js"></script>
<script src="js/workspace.js"></script>
<script src="js/auth.js"></script>
<script>window.CrankAuth.guardProtectedPage();</script>
+1
View File
@@ -13,6 +13,7 @@
<script src="js/config.js"></script>
<script src="js/i18n.js"></script>
<script src="js/api.js"></script>
<script src="js/ui-feedback.js"></script>
<script src="js/workspace.js"></script>
<script src="js/auth.js"></script>
<script>window.CrankAuth.guardProtectedPage();</script>
+4 -4
View File
@@ -319,12 +319,12 @@
</div>
<!-- Empty state -->
<div class="empty-state" x-show="!loading && totalFiltered === 0">
<h3 data-i18n="ops.empty.title">No operations found</h3>
<p data-i18n="ops.empty.sub">Try adjusting your search or filters.</p>
<div class="empty-state" x-show="!loading && !loadError && totalFiltered === 0">
<h3 x-text="operations.length === 0 ? 'No operations yet' : 'No operations found'">No operations found</h3>
<p x-text="operations.length === 0 ? 'Create the first operation in this workspace to expose a live MCP tool.' : 'Try adjusting your search or filters.'">Try adjusting your search or filters.</p>
</div>
<div class="empty-state" x-show="!loading && loadError">
<div class="empty-state" x-show="!loading && !!loadError">
<h3>Backend connection failed</h3>
<p x-text="loadError"></p>
</div>
+29 -1
View File
@@ -288,6 +288,12 @@ document.addEventListener('alpine:init', function() {
}
await this.reload();
if (window.CrankUi) {
window.CrankUi.success(
this.form.display_name + ' was saved with ' + this.form.selectedOps.length + ' tool bindings.',
this.drawerMode === 'create' ? 'Agent created' : 'Agent updated'
);
}
this.closeDrawer();
} catch (error) {
if (window.CrankUi) {
@@ -301,8 +307,15 @@ document.addEventListener('alpine:init', function() {
if (!confirm('Delete this agent? This cannot be undone.')) return;
try {
var agent = this.agents.find(function(item) { return item.id === id; });
await window.CrankApi.deleteAgent(this.workspaceId, id);
await this.reload();
if (window.CrankUi) {
window.CrankUi.success(
(agent ? agent.display_name : 'Agent') + ' was deleted.',
'Agent deleted'
);
}
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(error.message || 'Failed to delete agent', 'Delete failed');
@@ -326,6 +339,17 @@ document.addEventListener('alpine:init', function() {
await window.CrankApi.archiveAgent(this.workspaceId, agent.id);
}
await this.reload();
if (window.CrankUi) {
var actionLabel = action === 'publish'
? 'published'
: action === 'unpublish'
? 'returned to draft'
: 'archived';
window.CrankUi.success(
agent.display_name + ' was ' + actionLabel + '.',
'Agent lifecycle updated'
);
}
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(error.message || 'Failed to update agent lifecycle', 'Lifecycle update failed');
@@ -357,7 +381,11 @@ document.addEventListener('alpine:init', function() {
},
copyEndpoint(agent) {
navigator.clipboard.writeText(this.mcpEndpoint(agent)).catch(function() {});
var endpoint = this.mcpEndpoint(agent);
navigator.clipboard.writeText(endpoint).catch(function() {});
if (window.CrankUi) {
window.CrankUi.info(endpoint, 'MCP endpoint copied');
}
},
statusClass(status) {
+29
View File
@@ -69,8 +69,15 @@ async function revokeKey(id) {
if (!confirm('Revoke this key? All requests using it will fail immediately.')) return;
try {
var key = KEYS.find(function(item) { return item.id === id; });
await window.CrankApi.revokePlatformApiKey(currentWorkspaceId, id);
await loadKeys();
if (window.CrankUi) {
window.CrankUi.success(
(key ? key.name : 'API key') + ' was revoked.',
'Key revoked'
);
}
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(error.message || 'Failed to revoke key', 'Revoke failed');
@@ -82,8 +89,15 @@ async function deleteKey(id) {
if (!confirm('Delete this key? This cannot be undone.')) return;
try {
var key = KEYS.find(function(item) { return item.id === id; });
await window.CrankApi.deletePlatformApiKey(currentWorkspaceId, id);
await loadKeys();
if (window.CrankUi) {
window.CrankUi.success(
(key ? key.name : 'API key') + ' was deleted.',
'Key deleted'
);
}
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(error.message || 'Failed to delete key', 'Delete failed');
@@ -229,6 +243,7 @@ modal.addEventListener('click', function(event) {
});
document.getElementById('modal-confirm-btn').addEventListener('click', async function() {
var button = this;
var name = document.getElementById('new-key-name').value.trim();
if (!name) {
document.getElementById('new-key-name').focus();
@@ -242,6 +257,8 @@ document.getElementById('modal-confirm-btn').addEventListener('click', async fun
}
try {
button.disabled = true;
button.textContent = 'Creating…';
var created = await createKey(name, Array.from(selectedScopes));
KEYS.unshift(created.record);
document.getElementById('reveal-key-value').textContent = created.rawKey;
@@ -250,10 +267,16 @@ document.getElementById('modal-confirm-btn').addEventListener('click', async fun
document.getElementById('modal-footer-create').style.display = 'none';
document.getElementById('modal-footer-done').style.display = '';
renderTable();
if (window.CrankUi) {
window.CrankUi.success('Copy the key now. It will not be shown again.', 'API key created');
}
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(error.message || 'Failed to create key', 'Key creation failed');
}
} finally {
button.disabled = false;
button.textContent = 'Create key';
}
});
@@ -265,6 +288,9 @@ document.getElementById('copy-key-btn').addEventListener('click', function() {
navigator.clipboard.writeText(value).catch(function() {});
}
this.innerHTML = '<svg width="13" height="13"><use href="' + (window.APP_BASE || '') + 'icons/general/check.svg#icon"/></svg>';
if (window.CrankUi) {
window.CrankUi.info('Store the raw key securely. It cannot be revealed again.', 'API key copied');
}
});
document.getElementById('key-search').addEventListener('input', function() {
@@ -276,6 +302,9 @@ function copyPrefix(prefix) {
if (navigator.clipboard) {
navigator.clipboard.writeText(prefix).catch(function() {});
}
if (window.CrankUi) {
window.CrankUi.info(prefix, 'Key prefix copied');
}
}
document.addEventListener('DOMContentLoaded', async function() {
+7
View File
@@ -362,12 +362,19 @@ document.addEventListener('alpine:init', function() {
async deleteOperation(id) {
if (!confirm('Delete this operation? This cannot be undone.')) return;
try {
var operation = this.operations.find(function(item) { return item.id === id; });
await window.CrankApi.deleteOperation(this.workspaceId, id);
this.operations = this.operations.filter(function(operation) { return operation.id !== id; });
this.categoryOptions = Array.from(new Set(this.operations.map(function(operation) {
return operation.category;
}).filter(Boolean))).sort();
this.stats = computeStats(this.operations);
if (window.CrankUi) {
window.CrankUi.success(
(operation ? (operation.display_name || operation.name) : 'Operation') + ' was deleted.',
'Operation deleted'
);
}
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(error.message || 'Failed to delete operation', 'Delete failed');
+26 -7
View File
@@ -86,27 +86,34 @@ document.addEventListener('DOMContentLoaded', function () {
};
}
function renderEmpty(message) {
function renderEmpty(title, message) {
logList.innerHTML = '';
var empty = document.createElement('div');
empty.style.cssText = 'text-align:center;padding:40px;color:var(--text-muted);font-size:13.5px;';
empty.textContent = message;
empty.className = 'empty-state';
empty.innerHTML =
'<div class="empty-state-title">' + title + '</div>' +
'<div class="empty-state-text">' + message + '</div>';
logList.appendChild(empty);
}
function renderLogs() {
if (state.loading && state.logs.length === 0) {
renderEmpty('Loading logs...');
renderEmpty('Loading logs…', 'Fetching invocation history for the current workspace.');
return;
}
if (state.loadError) {
renderEmpty(state.loadError);
renderEmpty('Unable to load logs', state.loadError);
return;
}
if (!state.logs.length) {
renderEmpty('No log entries match the current filter');
renderEmpty(
'No log entries found',
state.search || state.level !== 'all'
? 'Try widening the period or clearing the current search and level filters.'
: 'Run tests or published MCP tools to start collecting invocation history.'
);
return;
}
@@ -323,6 +330,12 @@ document.addEventListener('DOMContentLoaded', function () {
state.liveMode = !state.liveMode;
setLiveState();
startPolling();
if (window.CrankUi) {
window.CrankUi.info(
state.liveMode ? 'Polling logs every 4 seconds.' : 'Automatic polling is paused.',
state.liveMode ? 'Live mode enabled' : 'Live mode paused'
);
}
}
document.querySelectorAll('.filter-chip[data-level]').forEach(function (button) {
@@ -344,7 +357,13 @@ document.addEventListener('DOMContentLoaded', function () {
}
if (refreshBtn) {
refreshBtn.addEventListener('click', loadLogs);
refreshBtn.addEventListener('click', function () {
loadLogs().then(function () {
if (!state.loadError && window.CrankUi) {
window.CrankUi.info('The latest invocation records were loaded for the current workspace.', 'Logs refreshed');
}
});
});
}
if (timeRangeSel) {
+33 -7
View File
@@ -76,9 +76,19 @@ document.addEventListener('DOMContentLoaded', function () {
return 'Wk ' + (index + 1);
}
function renderEmpty(message) {
chartWrap.innerHTML = '<div style="padding:32px;color:var(--text-muted);text-align:center;width:100%;">' + message + '</div>';
tableBody.innerHTML = '<tr><td colspan="7" style="padding:24px;text-align:center;color:var(--text-muted);">' + message + '</td></tr>';
function renderEmpty(title, message) {
chartWrap.innerHTML =
'<div class="empty-state" style="width:100%;">' +
'<div class="empty-state-title">' + title + '</div>' +
'<div class="empty-state-text">' + message + '</div>' +
'</div>';
tableBody.innerHTML =
'<tr><td colspan="7" style="padding:24px;">' +
'<div class="empty-state" style="padding:0;">' +
'<div class="empty-state-title">' + title + '</div>' +
'<div class="empty-state-text">' + message + '</div>' +
'</div>' +
'</td></tr>';
}
function renderStats() {
@@ -119,7 +129,11 @@ document.addEventListener('DOMContentLoaded', function () {
chartWrap.innerHTML = '';
if (!timeline.length) {
chartWrap.innerHTML = '<div style="padding:32px;color:var(--text-muted);text-align:center;width:100%;">No usage data for selected period</div>';
chartWrap.innerHTML =
'<div class="empty-state" style="width:100%;">' +
'<div class="empty-state-title">No usage data yet</div>' +
'<div class="empty-state-text">Invocation metrics will appear here after tests or published tool calls in the selected period.</div>' +
'</div>';
return;
}
@@ -145,7 +159,13 @@ document.addEventListener('DOMContentLoaded', function () {
tableBody.innerHTML = '';
if (!operations.length) {
tableBody.innerHTML = '<tr><td colspan="7" style="padding:24px;text-align:center;color:var(--text-muted);">No operation usage data for selected period</td></tr>';
tableBody.innerHTML =
'<tr><td colspan="7" style="padding:24px;">' +
'<div class="empty-state" style="padding:0;">' +
'<div class="empty-state-title">No operation breakdown yet</div>' +
'<div class="empty-state-text">The selected period has no invocation samples to break down by operation.</div>' +
'</div>' +
'</td></tr>';
return;
}
@@ -194,12 +214,12 @@ document.addEventListener('DOMContentLoaded', function () {
function renderUsage() {
if (state.loading && !state.usage) {
renderEmpty('Loading usage...');
renderEmpty('Loading usage…', 'Aggregating invocation metrics for the selected workspace.');
return;
}
if (state.loadError) {
renderEmpty(state.loadError);
renderEmpty('Unable to load usage', state.loadError);
return;
}
@@ -241,6 +261,9 @@ document.addEventListener('DOMContentLoaded', function () {
function exportCsv() {
if (!state.usage) {
if (window.CrankUi) {
window.CrankUi.info('Load usage data before exporting the CSV snapshot.', 'No usage data loaded');
}
return;
}
@@ -268,6 +291,9 @@ document.addEventListener('DOMContentLoaded', function () {
link.download = 'crank-usage-' + state.period + '.csv';
link.click();
URL.revokeObjectURL(url);
if (window.CrankUi) {
window.CrankUi.success('The current usage snapshot was exported as CSV.', 'Usage exported');
}
}
if (periodSelect) {