536 lines
17 KiB
JavaScript
536 lines
17 KiB
JavaScript
const PAGE_SIZE_DESKTOP = 6;
|
||
const PAGE_SIZE_MOBILE = 4;
|
||
const MOBILE_BREAKPOINT = 960;
|
||
|
||
function getSortOptions() {
|
||
return [
|
||
{ value: 'created_desc', label: (window.t && t('sort.created_desc')) || 'Sort: Last created' },
|
||
{ value: 'created_asc', label: (window.t && t('sort.created_asc')) || 'Sort: Oldest first' },
|
||
{ value: 'name_asc', label: (window.t && t('sort.name_asc')) || 'Sort: Name A–Z' },
|
||
{ value: 'name_desc', label: (window.t && t('sort.name_desc')) || 'Sort: Name Z–A' },
|
||
];
|
||
}
|
||
|
||
function getTabDefs() {
|
||
return [
|
||
{ id: 'all', label: (window.t && t('tab.all')) || 'All' },
|
||
{ id: 'active', label: (window.t && t('tab.active')) || 'Active' },
|
||
{ id: 'draft', label: (window.t && t('tab.draft')) || 'Draft' },
|
||
{ id: 'error', label: (window.t && t('tab.error')) || 'Error' },
|
||
{ id: 'inactive', label: (window.t && t('tab.inactive')) || 'Inactive' },
|
||
];
|
||
}
|
||
|
||
function uiStatus(rawStatus) {
|
||
if (rawStatus === 'published') return 'active';
|
||
if (rawStatus === 'archived') return 'inactive';
|
||
if (rawStatus === 'testing') return 'active';
|
||
return rawStatus || 'draft';
|
||
}
|
||
|
||
function currentLocale() {
|
||
return localStorage.getItem('crank_lang') === 'ru' ? 'ru-RU' : 'en-US';
|
||
}
|
||
|
||
function mapOperation(item) {
|
||
var mapped = {
|
||
id: item.id,
|
||
name: item.name,
|
||
display_name: item.display_name,
|
||
protocol: item.protocol,
|
||
status: uiStatus(item.status),
|
||
raw_status: item.status,
|
||
category: item.category || 'general',
|
||
created_at: item.created_at,
|
||
updated_at: item.updated_at,
|
||
published_at: item.published_at,
|
||
target_url: item.target_url || '',
|
||
method: item.target_action || '',
|
||
usage_summary: item.usage_summary || {
|
||
calls_today: 0,
|
||
error_rate_pct: 0,
|
||
avg_latency_ms: 0,
|
||
},
|
||
agent_refs: item.agent_refs || [],
|
||
};
|
||
|
||
return window.localizeDemoOperation ? window.localizeDemoOperation(mapped) : mapped;
|
||
}
|
||
|
||
function emptyStats() {
|
||
return {
|
||
total: 0,
|
||
active: 0,
|
||
requestsToday: 0,
|
||
averageLatencyMs: 0,
|
||
};
|
||
}
|
||
|
||
function computeStats(operations) {
|
||
if (!operations.length) return emptyStats();
|
||
|
||
var requestsToday = operations.reduce(function(sum, operation) {
|
||
return sum + (operation.usage_summary ? operation.usage_summary.calls_today : 0);
|
||
}, 0);
|
||
var latencyBase = operations.filter(function(operation) {
|
||
return operation.usage_summary && operation.usage_summary.calls_today > 0;
|
||
});
|
||
var latencyWeight = latencyBase.reduce(function(sum, operation) {
|
||
return sum + operation.usage_summary.calls_today;
|
||
}, 0);
|
||
var weightedLatency = latencyBase.reduce(function(sum, operation) {
|
||
return sum + (operation.usage_summary.avg_latency_ms * operation.usage_summary.calls_today);
|
||
}, 0);
|
||
|
||
return {
|
||
total: operations.length,
|
||
active: operations.filter(function(operation) { return operation.status === 'active'; }).length,
|
||
requestsToday: requestsToday,
|
||
averageLatencyMs: latencyWeight > 0 ? Math.round(weightedLatency / latencyWeight) : 0,
|
||
};
|
||
}
|
||
|
||
document.addEventListener('alpine:init', function() {
|
||
Alpine.data('catalog', function() {
|
||
return {
|
||
operations: [],
|
||
loading: true,
|
||
loadError: '',
|
||
pageSize: PAGE_SIZE_DESKTOP,
|
||
search: '',
|
||
tab: 'all',
|
||
filterProtocol: null,
|
||
filterCategory: null,
|
||
filterAgent: null,
|
||
sort: 'created_desc',
|
||
page: 1,
|
||
openDropdown: null,
|
||
navOpen: false,
|
||
categoryOptions: [],
|
||
workspaceId: null,
|
||
stats: emptyStats(),
|
||
_opsVersion: 0,
|
||
_nonStatusViewCache: null,
|
||
_nonStatusViewKey: '',
|
||
_filteredCache: null,
|
||
_filteredKey: '',
|
||
_agentsByOpIdCache: null,
|
||
_agentsByOpIdCacheVersion: -1,
|
||
_activeAgentsCache: null,
|
||
_activeAgentsCacheVersion: -1,
|
||
|
||
async init() {
|
||
var self = this;
|
||
|
||
await (window.whenWorkspacesReady ? window.whenWorkspacesReady() : Promise.resolve());
|
||
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
|
||
this.workspaceId = workspace ? workspace.id : null;
|
||
|
||
var updatePageSize = function() {
|
||
self.pageSize = window.innerWidth <= MOBILE_BREAKPOINT ? PAGE_SIZE_MOBILE : PAGE_SIZE_DESKTOP;
|
||
self.page = 1;
|
||
};
|
||
|
||
updatePageSize();
|
||
window.addEventListener('resize', updatePageSize);
|
||
window.addEventListener('crank:langchange', function() {
|
||
self.sort = self.sort;
|
||
applyLang();
|
||
});
|
||
window.addEventListener('crank:workspacechange', async function(event) {
|
||
self.workspaceId = event.detail ? event.detail.id : null;
|
||
await self.reload();
|
||
});
|
||
document.addEventListener('click', function(event) {
|
||
if (!event.target.closest('.filter-dropdown, .sort-dropdown, .user-menu, .ws-switcher')) {
|
||
self.openDropdown = null;
|
||
}
|
||
if (!event.target.closest('.navbar')) {
|
||
self.navOpen = false;
|
||
}
|
||
});
|
||
|
||
await this.reload();
|
||
},
|
||
|
||
async reload() {
|
||
this.loading = true;
|
||
this.loadError = '';
|
||
|
||
try {
|
||
var response = await window.CrankApi.listOperations(this.workspaceId);
|
||
this.replaceOperations((response && response.items ? response.items : []).map(mapOperation));
|
||
this.categoryOptions = Array.from(new Set(this.operations.map(function(operation) {
|
||
return operation.category;
|
||
}).filter(Boolean))).sort();
|
||
this.stats = computeStats(this.operations);
|
||
} catch (error) {
|
||
this.replaceOperations([]);
|
||
this.categoryOptions = [];
|
||
this.stats = emptyStats();
|
||
this.loadError = error.message || 'Failed to load operations';
|
||
}
|
||
|
||
this.loading = false;
|
||
this.page = 1;
|
||
},
|
||
|
||
replaceOperations(operations) {
|
||
this.operations = operations;
|
||
this._opsVersion += 1;
|
||
this._nonStatusViewCache = null;
|
||
this._nonStatusViewKey = '';
|
||
this._filteredCache = null;
|
||
this._filteredKey = '';
|
||
this._agentsByOpIdCache = null;
|
||
this._agentsByOpIdCacheVersion = -1;
|
||
this._activeAgentsCache = null;
|
||
this._activeAgentsCacheVersion = -1;
|
||
},
|
||
|
||
_applyNonStatusFilters(operations) {
|
||
var filtered = operations;
|
||
|
||
if (this.search.trim()) {
|
||
var query = this.search.toLowerCase();
|
||
filtered = filtered.filter(function(operation) {
|
||
return operation.name.toLowerCase().includes(query)
|
||
|| operation.display_name.toLowerCase().includes(query)
|
||
|| operation.category.toLowerCase().includes(query)
|
||
|| (operation.target_url || '').toLowerCase().includes(query);
|
||
});
|
||
}
|
||
|
||
if (this.filterProtocol) {
|
||
filtered = filtered.filter(function(operation) { return operation.protocol === this.filterProtocol; }, this);
|
||
}
|
||
|
||
if (this.filterCategory) {
|
||
filtered = filtered.filter(function(operation) { return operation.category === this.filterCategory; }, this);
|
||
}
|
||
|
||
if (this.filterAgent) {
|
||
filtered = filtered.filter(function(operation) {
|
||
return (operation.agent_refs || []).some(function(agent) { return agent.agent_id === this.filterAgent; }, this);
|
||
}, this);
|
||
}
|
||
|
||
return filtered;
|
||
},
|
||
|
||
_nonStatusView() {
|
||
var key = [
|
||
this._opsVersion,
|
||
this.search.trim(),
|
||
this.filterProtocol || '',
|
||
this.filterCategory || '',
|
||
this.filterAgent || '',
|
||
].join('|');
|
||
|
||
if (this._nonStatusViewCache && this._nonStatusViewKey === key) {
|
||
return this._nonStatusViewCache;
|
||
}
|
||
|
||
var operations = this._applyNonStatusFilters(this.operations);
|
||
var tabCounts = {
|
||
all: operations.length,
|
||
active: 0,
|
||
draft: 0,
|
||
error: 0,
|
||
inactive: 0,
|
||
};
|
||
|
||
operations.forEach(function(operation) {
|
||
if (Object.prototype.hasOwnProperty.call(tabCounts, operation.status)) {
|
||
tabCounts[operation.status] += 1;
|
||
}
|
||
});
|
||
|
||
this._nonStatusViewKey = key;
|
||
this._nonStatusViewCache = {
|
||
operations: operations,
|
||
tabCounts: tabCounts,
|
||
};
|
||
return this._nonStatusViewCache;
|
||
},
|
||
|
||
get filtered() {
|
||
var nonStatusView = this._nonStatusView();
|
||
var key = [
|
||
this._nonStatusViewKey,
|
||
this.tab,
|
||
this.sort,
|
||
].join('|');
|
||
|
||
if (this._filteredCache && this._filteredKey === key) {
|
||
return this._filteredCache;
|
||
}
|
||
|
||
var operations = nonStatusView.operations;
|
||
|
||
if (this.tab !== 'all') {
|
||
operations = operations.filter(function(operation) {
|
||
return operation.status === this.tab;
|
||
}, this);
|
||
}
|
||
|
||
operations = operations.slice().sort(function(left, right) {
|
||
if (this.sort === 'created_desc') return new Date(right.created_at) - new Date(left.created_at);
|
||
if (this.sort === 'created_asc') return new Date(left.created_at) - new Date(right.created_at);
|
||
if (this.sort === 'name_asc') return left.name.localeCompare(right.name);
|
||
if (this.sort === 'name_desc') return right.name.localeCompare(left.name);
|
||
return 0;
|
||
}.bind(this));
|
||
|
||
this._filteredKey = key;
|
||
this._filteredCache = operations;
|
||
return this._filteredCache;
|
||
},
|
||
|
||
get totalFiltered() {
|
||
return this.filtered.length;
|
||
},
|
||
|
||
get totalPages() {
|
||
return Math.max(1, Math.ceil(this.totalFiltered / this.pageSize));
|
||
},
|
||
|
||
get paginated() {
|
||
var start = (this.page - 1) * this.pageSize;
|
||
return this.filtered.slice(start, start + this.pageSize);
|
||
},
|
||
|
||
get pageStart() {
|
||
return this.totalFiltered === 0 ? 0 : (this.page - 1) * this.pageSize + 1;
|
||
},
|
||
|
||
get pageEnd() {
|
||
return Math.min(this.page * this.pageSize, this.totalFiltered);
|
||
},
|
||
|
||
tabCount(tab) {
|
||
var tabCounts = this._nonStatusView().tabCounts;
|
||
return Object.prototype.hasOwnProperty.call(tabCounts, tab) ? tabCounts[tab] : 0;
|
||
},
|
||
|
||
get activeChips() {
|
||
var chips = [];
|
||
if (this.filterProtocol) chips.push({ key: 'protocol', label: this.tfKey('ops.filter.protocol', { value: this.filterProtocol.toUpperCase() }) });
|
||
if (this.filterCategory) chips.push({ key: 'category', label: this.tfKey('ops.filter.category', { value: this.filterCategory }) });
|
||
if (this.filterAgent) {
|
||
var agent = this.activeAgents.find(function(item) { return item.agent_id === this.filterAgent; }, this);
|
||
chips.push({ key: 'agent', label: this.tfKey('ops.filter.agent', { value: agent ? agent.display_name : '' }) });
|
||
}
|
||
return chips;
|
||
},
|
||
|
||
get hasActiveFilters() {
|
||
return !!(this.filterProtocol || this.filterCategory || this.filterAgent || this.search.trim());
|
||
},
|
||
|
||
clearChip(key) {
|
||
if (key === 'protocol') this.filterProtocol = null;
|
||
if (key === 'category') this.filterCategory = null;
|
||
if (key === 'agent') this.filterAgent = null;
|
||
this.page = 1;
|
||
},
|
||
|
||
clearAllFilters() {
|
||
this.filterProtocol = null;
|
||
this.filterCategory = null;
|
||
this.filterAgent = null;
|
||
this.search = '';
|
||
this.tab = 'all';
|
||
this.page = 1;
|
||
},
|
||
|
||
get agentsByOpId() {
|
||
if (this._agentsByOpIdCache && this._agentsByOpIdCacheVersion === this._opsVersion) {
|
||
return this._agentsByOpIdCache;
|
||
}
|
||
var map = {};
|
||
this.operations.forEach(function(operation) {
|
||
map[operation.id] = operation.agent_refs || [];
|
||
});
|
||
this._agentsByOpIdCacheVersion = this._opsVersion;
|
||
this._agentsByOpIdCache = map;
|
||
return this._agentsByOpIdCache;
|
||
},
|
||
|
||
get activeAgents() {
|
||
if (this._activeAgentsCache && this._activeAgentsCacheVersion === this._opsVersion) {
|
||
return this._activeAgentsCache;
|
||
}
|
||
var seen = {};
|
||
var agents = [];
|
||
this.operations.forEach(function(operation) {
|
||
(operation.agent_refs || []).forEach(function(agent) {
|
||
if (!seen[agent.agent_id]) {
|
||
seen[agent.agent_id] = true;
|
||
agents.push(agent);
|
||
}
|
||
});
|
||
});
|
||
this._activeAgentsCacheVersion = this._opsVersion;
|
||
this._activeAgentsCache = agents.sort(function(left, right) {
|
||
return left.display_name.localeCompare(right.display_name);
|
||
});
|
||
return this._activeAgentsCache;
|
||
},
|
||
|
||
get sortLabel() {
|
||
return getSortOptions().find(function(option) { return option.value === this.sort; }, this)?.label || 'Sort';
|
||
},
|
||
|
||
setTab(tab) {
|
||
this.tab = tab;
|
||
this.page = 1;
|
||
},
|
||
|
||
setSearch(value) {
|
||
this.search = value;
|
||
this.page = 1;
|
||
},
|
||
|
||
setProtocol(value) {
|
||
this.filterProtocol = this.filterProtocol === value ? null : value;
|
||
this.openDropdown = null;
|
||
this.page = 1;
|
||
},
|
||
|
||
setCategory(value) {
|
||
this.filterCategory = this.filterCategory === value ? null : value;
|
||
this.openDropdown = null;
|
||
this.page = 1;
|
||
},
|
||
|
||
setSort(value) {
|
||
this.sort = value;
|
||
this.openDropdown = null;
|
||
},
|
||
|
||
setAgent(value) {
|
||
this.filterAgent = this.filterAgent === value ? null : value;
|
||
this.openDropdown = null;
|
||
this.page = 1;
|
||
},
|
||
|
||
clearProtocol() {
|
||
this.filterProtocol = null;
|
||
this.openDropdown = null;
|
||
this.page = 1;
|
||
},
|
||
|
||
clearCategory() {
|
||
this.filterCategory = null;
|
||
this.openDropdown = null;
|
||
this.page = 1;
|
||
},
|
||
|
||
clearAgent() {
|
||
this.filterAgent = null;
|
||
this.openDropdown = null;
|
||
this.page = 1;
|
||
},
|
||
|
||
toggleDropdown(name) {
|
||
this.openDropdown = this.openDropdown === name ? null : name;
|
||
},
|
||
|
||
goPage(page) {
|
||
if (page >= 1 && page <= this.totalPages) this.page = page;
|
||
},
|
||
|
||
editOperation(operation) {
|
||
window.location.href = ((window.CrankRoutes && window.CrankRoutes.wizard) || '/wizard/')
|
||
+ '?mode=edit&operationId=' + encodeURIComponent(operation.id);
|
||
},
|
||
|
||
async deleteOperation(id) {
|
||
if (!confirm(this.tKey('ops.delete.confirm'))) return;
|
||
try {
|
||
var operation = this.operations.find(function(item) { return item.id === id; });
|
||
await window.CrankApi.deleteOperation(this.workspaceId, id);
|
||
this.replaceOperations(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(
|
||
this.tfKey('ops.delete.success.message', {
|
||
name: operation ? (operation.display_name || operation.name) : ''
|
||
}),
|
||
this.tKey('ops.delete.success.title')
|
||
);
|
||
}
|
||
} catch (error) {
|
||
if (window.CrankUi) {
|
||
window.CrankUi.error(
|
||
error.message || this.tKey('ops.delete.error.message'),
|
||
this.tKey('ops.delete.error.title')
|
||
);
|
||
}
|
||
}
|
||
},
|
||
|
||
protocolLabel(operation) {
|
||
return operation.method ? ('REST · ' + operation.method) : 'REST';
|
||
},
|
||
|
||
protocolClass(operation) {
|
||
return 'badge badge-' + operation.protocol;
|
||
},
|
||
|
||
statusClass(operation) {
|
||
return 'status-badge status-' + operation.status;
|
||
},
|
||
|
||
statusLabel(operation) {
|
||
if (operation.status === 'active') return this.tKey('tab.active');
|
||
if (operation.status === 'inactive') return this.tKey('tab.inactive');
|
||
if (operation.status === 'error') return this.tKey('tab.error');
|
||
return this.tKey('tab.draft');
|
||
},
|
||
|
||
formatDate(dateStr) {
|
||
if (!dateStr) return '—';
|
||
return new Date(dateStr).toLocaleDateString(currentLocale(), { month: 'short', day: 'numeric', year: 'numeric' });
|
||
},
|
||
|
||
truncateUrl(url) {
|
||
if (!url) return '—';
|
||
return url.length > 42 ? url.slice(0, 42) + '…' : url;
|
||
},
|
||
|
||
formatMetricNumber(value) {
|
||
return new Intl.NumberFormat(currentLocale()).format(value || 0);
|
||
},
|
||
|
||
tKey(key) {
|
||
return window.t ? t(key) : key;
|
||
},
|
||
|
||
tfKey(key, vars) {
|
||
return window.tf ? tf(key, vars) : this.tKey(key);
|
||
},
|
||
|
||
get sortOptions() {
|
||
return getSortOptions();
|
||
},
|
||
|
||
get tabDefs() {
|
||
return getTabDefs();
|
||
},
|
||
|
||
handleNewOperation() {
|
||
window.location.href = (window.CrankRoutes && window.CrankRoutes.wizard) || '/wizard/';
|
||
},
|
||
|
||
handleLogout() {
|
||
window.CrankAuth.logout();
|
||
},
|
||
};
|
||
});
|
||
});
|