ui: cache derived catalog views
This commit is contained in:
@@ -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
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user