feat: connect alpine operations and wizard to admin api
This commit is contained in:
+380
-282
@@ -1,333 +1,431 @@
|
||||
const PAGE_SIZE_DESKTOP = 6;
|
||||
const PAGE_SIZE_MOBILE = 4;
|
||||
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' },
|
||||
{ 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: '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', () => {
|
||||
function uiStatus(rawStatus) {
|
||||
if (rawStatus === 'published') return 'active';
|
||||
if (rawStatus === 'archived') return 'inactive';
|
||||
if (rawStatus === 'testing') return 'active';
|
||||
return rawStatus || 'draft';
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
function mapOperation(item) {
|
||||
return {
|
||||
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: '',
|
||||
method: '',
|
||||
usage_summary: item.usage_summary || {
|
||||
calls_today: 0,
|
||||
error_rate_pct: 0,
|
||||
avg_latency_ms: 0,
|
||||
},
|
||||
agent_refs: item.agent_refs || [],
|
||||
};
|
||||
}
|
||||
|
||||
// ── 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;
|
||||
},
|
||||
function emptyStats() {
|
||||
return {
|
||||
total: 0,
|
||||
active: 0,
|
||||
requestsToday: 0,
|
||||
averageLatencyMs: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// ── computed: filtered + sorted + paginated ──
|
||||
get filtered() {
|
||||
let ops = this._applyNonStatusFilters(this.operations);
|
||||
function computeStats(operations) {
|
||||
if (!operations.length) return emptyStats();
|
||||
|
||||
if (this.tab !== 'all') {
|
||||
ops = ops.filter(op => op.status === this.tab);
|
||||
}
|
||||
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);
|
||||
|
||||
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 {
|
||||
total: operations.length,
|
||||
active: operations.filter(function(operation) { return operation.status === 'active'; }).length,
|
||||
requestsToday: requestsToday,
|
||||
averageLatencyMs: latencyWeight > 0 ? Math.round(weightedLatency / latencyWeight) : 0,
|
||||
};
|
||||
}
|
||||
|
||||
return ops;
|
||||
},
|
||||
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(),
|
||||
|
||||
get totalFiltered() { return this.filtered.length; },
|
||||
get totalPages() { return Math.max(1, Math.ceil(this.totalFiltered / this.pageSize)); },
|
||||
async init() {
|
||||
var self = this;
|
||||
|
||||
get paginated() {
|
||||
const start = (this.page - 1) * this.pageSize;
|
||||
return this.filtered.slice(start, start + this.pageSize);
|
||||
},
|
||||
await (window.whenWorkspacesReady ? window.whenWorkspacesReady() : Promise.resolve());
|
||||
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
|
||||
this.workspaceId = workspace ? workspace.id : null;
|
||||
|
||||
get pageStart() { return this.totalFiltered === 0 ? 0 : (this.page - 1) * this.pageSize + 1; },
|
||||
get pageEnd() { return Math.min(this.page * this.pageSize, this.totalFiltered); },
|
||||
var updatePageSize = function() {
|
||||
self.pageSize = window.innerWidth <= MOBILE_BREAKPOINT ? PAGE_SIZE_MOBILE : PAGE_SIZE_DESKTOP;
|
||||
self.page = 1;
|
||||
};
|
||||
|
||||
// ── 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);
|
||||
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')) {
|
||||
self.openDropdown = null;
|
||||
}
|
||||
if (!event.target.closest('.navbar')) {
|
||||
self.navOpen = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
return map;
|
||||
},
|
||||
|
||||
// ── active agents only (for filter dropdown) ──
|
||||
get activeAgents() {
|
||||
return this.agents.filter(a => a.status === 'active');
|
||||
},
|
||||
await this.reload();
|
||||
},
|
||||
|
||||
// ── sort label ──
|
||||
get sortLabel() {
|
||||
return getSortOptions().find(o => o.value === this.sort)?.label ?? 'Sort';
|
||||
},
|
||||
async reload() {
|
||||
this.loading = true;
|
||||
this.loadError = '';
|
||||
|
||||
// ── actions ──
|
||||
setTab(tab) {
|
||||
this.tab = tab;
|
||||
this.page = 1;
|
||||
},
|
||||
try {
|
||||
var response = await window.CrankApi.listOperations(this.workspaceId);
|
||||
this.operations = (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.operations = [];
|
||||
this.categoryOptions = [];
|
||||
this.stats = emptyStats();
|
||||
this.loadError = error.message || 'Failed to load operations';
|
||||
}
|
||||
|
||||
setSearch(val) {
|
||||
this.search = val;
|
||||
this.page = 1;
|
||||
},
|
||||
this.loading = false;
|
||||
this.page = 1;
|
||||
},
|
||||
|
||||
setProtocol(val) {
|
||||
this.filterProtocol = this.filterProtocol === val ? null : val;
|
||||
this.openDropdown = null;
|
||||
this.page = 1;
|
||||
},
|
||||
_applyNonStatusFilters(operations) {
|
||||
var filtered = operations;
|
||||
|
||||
setCategory(val) {
|
||||
this.filterCategory = this.filterCategory === val ? null : val;
|
||||
this.openDropdown = null;
|
||||
this.page = 1;
|
||||
},
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
setSort(val) {
|
||||
this.sort = val;
|
||||
this.openDropdown = null;
|
||||
},
|
||||
if (this.filterProtocol) {
|
||||
filtered = filtered.filter(function(operation) { return operation.protocol === this.filterProtocol; }, this);
|
||||
}
|
||||
|
||||
setAgent(val) {
|
||||
this.filterAgent = this.filterAgent === val ? null : val;
|
||||
this.openDropdown = null;
|
||||
this.page = 1;
|
||||
},
|
||||
if (this.filterCategory) {
|
||||
filtered = filtered.filter(function(operation) { return operation.category === this.filterCategory; }, this);
|
||||
}
|
||||
|
||||
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; },
|
||||
if (this.filterAgent) {
|
||||
filtered = filtered.filter(function(operation) {
|
||||
return (operation.agent_refs || []).some(function(agent) { return agent.agent_id === this.filterAgent; }, this);
|
||||
}, this);
|
||||
}
|
||||
|
||||
toggleDropdown(name) {
|
||||
this.openDropdown = this.openDropdown === name ? null : name;
|
||||
},
|
||||
return filtered;
|
||||
},
|
||||
|
||||
goPage(p) {
|
||||
if (p >= 1 && p <= this.totalPages) this.page = p;
|
||||
},
|
||||
get filtered() {
|
||||
var operations = this._applyNonStatusFilters(this.operations);
|
||||
|
||||
// ── operation CRUD ──
|
||||
editOperation(op) {
|
||||
try { sessionStorage.setItem('wizard_edit', JSON.stringify(op)); } catch (e) {}
|
||||
window.location.href = (window.APP_BASE||'')+'html/wizard/?mode=edit';
|
||||
},
|
||||
if (this.tab !== 'all') {
|
||||
operations = operations.filter(function(operation) {
|
||||
return operation.status === this.tab;
|
||||
}, this);
|
||||
}
|
||||
|
||||
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) {}
|
||||
},
|
||||
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));
|
||||
|
||||
// ── helpers для шаблона ──
|
||||
protocolLabel(op) {
|
||||
if (op.protocol === 'rest') return `REST · ${op.method}`;
|
||||
if (op.protocol === 'graphql') return 'GraphQL';
|
||||
return 'gRPC';
|
||||
},
|
||||
return operations;
|
||||
},
|
||||
|
||||
protocolClass(op) {
|
||||
return `badge badge-${op.protocol}`;
|
||||
},
|
||||
get totalFiltered() {
|
||||
return this.filtered.length;
|
||||
},
|
||||
|
||||
statusClass(op) {
|
||||
return `status-badge status-${op.status}`;
|
||||
},
|
||||
get totalPages() {
|
||||
return Math.max(1, Math.ceil(this.totalFiltered / this.pageSize));
|
||||
},
|
||||
|
||||
statusLabel(op) {
|
||||
return op.status.charAt(0).toUpperCase() + op.status.slice(1);
|
||||
},
|
||||
get paginated() {
|
||||
var start = (this.page - 1) * this.pageSize;
|
||||
return this.filtered.slice(start, start + this.pageSize);
|
||||
},
|
||||
|
||||
formatDate(dateStr) {
|
||||
if (!dateStr) return '—';
|
||||
return new Date(dateStr).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
||||
},
|
||||
get pageStart() {
|
||||
return this.totalFiltered === 0 ? 0 : (this.page - 1) * this.pageSize + 1;
|
||||
},
|
||||
|
||||
truncateUrl(url) {
|
||||
if (!url) return '';
|
||||
return url.length > 42 ? url.slice(0, 42) + '…' : url;
|
||||
},
|
||||
get pageEnd() {
|
||||
return Math.min(this.page * this.pageSize, this.totalFiltered);
|
||||
},
|
||||
|
||||
get sortOptions() { return getSortOptions(); },
|
||||
get tabDefs() { return getTabDefs(); },
|
||||
tabCount(tab) {
|
||||
var operations = this._applyNonStatusFilters(this.operations);
|
||||
if (tab === 'all') return operations.length;
|
||||
return operations.filter(function(operation) { return operation.status === tab; }).length;
|
||||
},
|
||||
|
||||
handleNewOperation() {
|
||||
sessionStorage.removeItem('wizard_edit');
|
||||
window.location.href = (window.APP_BASE||'')+'html/wizard/';
|
||||
},
|
||||
get activeChips() {
|
||||
var 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) {
|
||||
var agent = this.activeAgents.find(function(item) { return item.agent_id === this.filterAgent; }, this);
|
||||
chips.push({ key: 'agent', label: 'Agent: ' + (agent ? agent.display_name : '') });
|
||||
}
|
||||
return chips;
|
||||
},
|
||||
|
||||
handleLogout() {
|
||||
localStorage.removeItem('crank_user');
|
||||
window.location.href = (window.APP_BASE||'')+'html/login.html';
|
||||
},
|
||||
}));
|
||||
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() {
|
||||
var map = {};
|
||||
this.operations.forEach(function(operation) {
|
||||
map[operation.id] = operation.agent_refs || [];
|
||||
});
|
||||
return map;
|
||||
},
|
||||
|
||||
get activeAgents() {
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
return agents.sort(function(left, right) {
|
||||
return left.display_name.localeCompare(right.display_name);
|
||||
});
|
||||
},
|
||||
|
||||
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.APP_BASE || '') + 'html/wizard/?mode=edit&operationId=' + encodeURIComponent(operation.id);
|
||||
},
|
||||
|
||||
async deleteOperation(id) {
|
||||
if (!confirm('Delete this operation? This cannot be undone.')) return;
|
||||
try {
|
||||
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);
|
||||
} catch (error) {
|
||||
alert(error.message || 'Failed to delete operation');
|
||||
}
|
||||
},
|
||||
|
||||
protocolLabel(operation) {
|
||||
if (operation.protocol === 'rest') {
|
||||
return operation.method ? ('REST · ' + operation.method) : 'REST';
|
||||
}
|
||||
if (operation.protocol === 'graphql') return 'GraphQL';
|
||||
return 'gRPC';
|
||||
},
|
||||
|
||||
protocolClass(operation) {
|
||||
return 'badge badge-' + operation.protocol;
|
||||
},
|
||||
|
||||
statusClass(operation) {
|
||||
return 'status-badge status-' + operation.status;
|
||||
},
|
||||
|
||||
statusLabel(operation) {
|
||||
if (operation.status === 'active') return 'Active';
|
||||
if (operation.status === 'inactive') return 'Inactive';
|
||||
if (operation.status === 'error') return 'Error';
|
||||
return operation.status.charAt(0).toUpperCase() + operation.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;
|
||||
},
|
||||
|
||||
formatMetricNumber(value) {
|
||||
return new Intl.NumberFormat('en-US').format(value || 0);
|
||||
},
|
||||
|
||||
get sortOptions() {
|
||||
return getSortOptions();
|
||||
},
|
||||
|
||||
get tabDefs() {
|
||||
return getTabDefs();
|
||||
},
|
||||
|
||||
handleNewOperation() {
|
||||
window.location.href = (window.APP_BASE || '') + 'html/wizard/';
|
||||
},
|
||||
|
||||
handleLogout() {
|
||||
localStorage.removeItem('crank_user');
|
||||
window.location.href = (window.APP_BASE || '') + 'html/login.html';
|
||||
},
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user