feat: polish alpine live page states
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user