feat: migrate alpine ui into app shell

This commit is contained in:
a.tolmachev
2026-03-30 00:15:02 +03:00
parent f33aa29c88
commit 7070231c3e
84 changed files with 12530 additions and 92 deletions
+333
View File
@@ -0,0 +1,333 @@
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';
},
}));
});