ui: cache derived catalog views

This commit is contained in:
a.tolmachev
2026-04-12 02:32:21 +03:00
parent 4d7b11ec96
commit d85f7ddf05
2 changed files with 95 additions and 15 deletions
+5 -5
View File
@@ -2,18 +2,18 @@
## Current
### `feat/frontend-performance-polish`
### `feat/frontend-observability-and-testability`
Status: in_progress
DoD:
- repeated O(n) filtering in hot render paths is removed
- tab counts and similar derived UI metrics use cached/derived snapshots instead of recomputing the same filters
- existing UI e2e coverage stays green after the performance polish
- UI modules expose stable hooks for diagnostics and targeted testing where the current DOM is ambiguous
- console/runtime failures on critical pages are easier to attribute to a concrete page module
- existing UI e2e coverage stays green after the observability/testability pass
## Next
- `feat/frontend-observability-and-testability`
- `feat/id-display-support`
## Backlog
+90 -10
View File
@@ -109,6 +109,15 @@ document.addEventListener('alpine:init', function() {
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;
@@ -150,13 +159,13 @@ document.addEventListener('alpine:init', function() {
try {
var response = await window.CrankApi.listOperations(this.workspaceId);
this.operations = (response && response.items ? response.items : []).map(mapOperation);
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.operations = [];
this.replaceOperations([]);
this.categoryOptions = [];
this.stats = emptyStats();
this.loadError = error.message || 'Failed to load operations';
@@ -166,6 +175,19 @@ document.addEventListener('alpine:init', function() {
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;
@@ -196,8 +218,55 @@ document.addEventListener('alpine:init', function() {
return filtered;
},
get 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) {
@@ -213,7 +282,9 @@ document.addEventListener('alpine:init', function() {
return 0;
}.bind(this));
return operations;
this._filteredKey = key;
this._filteredCache = operations;
return this._filteredCache;
},
get totalFiltered() {
@@ -238,9 +309,8 @@ document.addEventListener('alpine:init', function() {
},
tabCount(tab) {
var operations = this._applyNonStatusFilters(this.operations);
if (tab === 'all') return operations.length;
return operations.filter(function(operation) { return operation.status === tab; }).length;
var tabCounts = this._nonStatusView().tabCounts;
return Object.prototype.hasOwnProperty.call(tabCounts, tab) ? tabCounts[tab] : 0;
},
get activeChips() {
@@ -275,14 +345,22 @@ document.addEventListener('alpine:init', function() {
},
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 || [];
});
return map;
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) {
@@ -293,9 +371,11 @@ document.addEventListener('alpine:init', function() {
}
});
});
return agents.sort(function(left, right) {
this._activeAgentsCacheVersion = this._opsVersion;
this._activeAgentsCache = agents.sort(function(left, right) {
return left.display_name.localeCompare(right.display_name);
});
return this._activeAgentsCache;
},
get sortLabel() {
@@ -371,7 +451,7 @@ document.addEventListener('alpine:init', function() {
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.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();