Files
crank/apps/ui/js/catalog.js
T
2026-03-30 00:15:02 +03:00

334 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 AZ' },
{ value: 'name_desc', label: (window.t && t('sort.name_desc')) || 'Sort: Name ZA' },
];
}
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' },
];
}
document.addEventListener('alpine:init', () => {
Alpine.data('catalog', () => ({
// ── raw data ──
operations: [],
agents: [],
loading: true,
// ── page size (responsive) ──
pageSize: PAGE_SIZE_DESKTOP,
// ── filter state ──
search: '',
tab: 'all',
filterProtocol: null,
filterCategory: null,
filterAgent: null,
sort: 'created_desc',
page: 1,
// ── dropdown open state ──
openDropdown: null,
// ── mobile nav ──
navOpen: false,
// ── derived option lists (built after data loads) ──
categoryOptions: [],
async init() {
const [opsRes, agentsRes] = await Promise.all([
fetch((window.DATA_URL||'data/')+'operations.json'),
fetch((window.DATA_URL||'data/')+'agents.json'),
]);
let ops = await opsRes.json();
this.agents = await agentsRes.json();
// Apply localStorage overrides (from wizard create/edit)
try {
const overrides = JSON.parse(localStorage.getItem('crank_ops_overrides') || '[]');
if (overrides.length) {
const existingIds = new Set(ops.map(o => o.id));
ops = ops.map(op => {
const ov = overrides.find(o => o.id === op.id);
return ov ? Object.assign({}, op, ov) : op;
});
overrides
.filter(o => !existingIds.has(o.id))
.forEach(o => ops.push(o));
}
} catch (e) {}
this.operations = ops;
this.categoryOptions = [...new Set(this.operations.map(op => op.category))].filter(Boolean).sort();
this.loading = false;
// Responsive page size
const updatePageSize = () => {
this.pageSize = window.innerWidth <= MOBILE_BREAKPOINT ? PAGE_SIZE_MOBILE : PAGE_SIZE_DESKTOP;
this.page = 1;
};
updatePageSize();
window.addEventListener('resize', updatePageSize);
// Re-render labels on language change
window.addEventListener('crank:langchange', () => {
this.sort = this.sort;
applyLang();
});
// Close dropdowns and mobile nav on outside click
document.addEventListener('click', (e) => {
if (!e.target.closest('.filter-dropdown, .sort-dropdown, .user-menu')) {
this.openDropdown = null;
}
if (!e.target.closest('.navbar')) {
this.navOpen = false;
}
});
},
// ── helpers for filtering (shared between filtered getter and tabCount) ──
_applyNonStatusFilters(ops) {
if (this.search.trim()) {
const q = this.search.toLowerCase();
ops = ops.filter(op =>
op.name.toLowerCase().includes(q) ||
op.display_name.toLowerCase().includes(q) ||
(op.target_url || '').toLowerCase().includes(q)
);
}
if (this.filterProtocol) {
ops = ops.filter(op => op.protocol === this.filterProtocol);
}
if (this.filterCategory) {
ops = ops.filter(op => op.category === this.filterCategory);
}
if (this.filterAgent) {
const agent = this.agents.find(a => a.id === this.filterAgent);
const ids = agent ? agent.operation_ids : [];
ops = ops.filter(op => ids.includes(op.id));
}
return ops;
},
// ── computed: filtered + sorted + paginated ──
get filtered() {
let ops = this._applyNonStatusFilters(this.operations);
if (this.tab !== 'all') {
ops = ops.filter(op => op.status === this.tab);
}
ops = [...ops].sort((a, b) => {
switch (this.sort) {
case 'created_desc': return new Date(b.created_at) - new Date(a.created_at);
case 'created_asc': return new Date(a.created_at) - new Date(b.created_at);
case 'name_asc': return a.name.localeCompare(b.name);
case 'name_desc': return b.name.localeCompare(a.name);
default: return 0;
}
});
return ops;
},
get totalFiltered() { return this.filtered.length; },
get totalPages() { return Math.max(1, Math.ceil(this.totalFiltered / this.pageSize)); },
get paginated() {
const 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); },
// ── tab counts (respect non-status filters so numbers are consistent) ──
tabCount(tab) {
const ops = this._applyNonStatusFilters(this.operations);
if (tab === 'all') return ops.length;
return ops.filter(op => op.status === tab).length;
},
// ── active filter chips (for dismissible chip row) ──
get activeChips() {
const chips = [];
if (this.filterProtocol) {
chips.push({ key: 'protocol', label: 'Protocol: ' + this.filterProtocol.toUpperCase() });
}
if (this.filterCategory) {
chips.push({ key: 'category', label: 'Category: ' + this.filterCategory });
}
if (this.filterAgent) {
const a = this.agents.find(ag => ag.id === this.filterAgent);
chips.push({ key: 'agent', label: 'Agent: ' + (a ? a.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;
},
// ── agent lookup per operation (for badges in rows) ──
get agentsByOpId() {
const map = {};
this.agents.forEach(a => {
a.operation_ids.forEach(id => {
if (!map[id]) map[id] = [];
map[id].push(a);
});
});
return map;
},
// ── active agents only (for filter dropdown) ──
get activeAgents() {
return this.agents.filter(a => a.status === 'active');
},
// ── sort label ──
get sortLabel() {
return getSortOptions().find(o => o.value === this.sort)?.label ?? 'Sort';
},
// ── actions ──
setTab(tab) {
this.tab = tab;
this.page = 1;
},
setSearch(val) {
this.search = val;
this.page = 1;
},
setProtocol(val) {
this.filterProtocol = this.filterProtocol === val ? null : val;
this.openDropdown = null;
this.page = 1;
},
setCategory(val) {
this.filterCategory = this.filterCategory === val ? null : val;
this.openDropdown = null;
this.page = 1;
},
setSort(val) {
this.sort = val;
this.openDropdown = null;
},
setAgent(val) {
this.filterAgent = this.filterAgent === val ? null : val;
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(p) {
if (p >= 1 && p <= this.totalPages) this.page = p;
},
// ── operation CRUD ──
editOperation(op) {
try { sessionStorage.setItem('wizard_edit', JSON.stringify(op)); } catch (e) {}
window.location.href = (window.APP_BASE||'')+'html/wizard/?mode=edit';
},
deleteOperation(id) {
if (!confirm('Delete this operation? This cannot be undone.')) return;
this.operations = this.operations.filter(op => op.id !== id);
// Persist deletion in overrides as a tombstone
try {
const overrides = JSON.parse(localStorage.getItem('crank_ops_overrides') || '[]');
const filtered = overrides.filter(o => o.id !== id);
filtered.push({ id, _deleted: true });
localStorage.setItem('crank_ops_overrides', JSON.stringify(filtered));
} catch (e) {}
},
// ── helpers для шаблона ──
protocolLabel(op) {
if (op.protocol === 'rest') return `REST · ${op.method}`;
if (op.protocol === 'graphql') return 'GraphQL';
return 'gRPC';
},
protocolClass(op) {
return `badge badge-${op.protocol}`;
},
statusClass(op) {
return `status-badge status-${op.status}`;
},
statusLabel(op) {
return op.status.charAt(0).toUpperCase() + op.status.slice(1);
},
formatDate(dateStr) {
if (!dateStr) return '—';
return new Date(dateStr).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
},
truncateUrl(url) {
if (!url) return '';
return url.length > 42 ? url.slice(0, 42) + '…' : url;
},
get sortOptions() { return getSortOptions(); },
get tabDefs() { return getTabDefs(); },
handleNewOperation() {
sessionStorage.removeItem('wizard_edit');
window.location.href = (window.APP_BASE||'')+'html/wizard/';
},
handleLogout() {
localStorage.removeItem('crank_user');
window.location.href = (window.APP_BASE||'')+'html/login.html';
},
}));
});