ui: cache derived catalog views
This commit is contained in:
@@ -2,18 +2,18 @@
|
|||||||
|
|
||||||
## Current
|
## Current
|
||||||
|
|
||||||
### `feat/frontend-performance-polish`
|
### `feat/frontend-observability-and-testability`
|
||||||
|
|
||||||
Status: in_progress
|
Status: in_progress
|
||||||
|
|
||||||
DoD:
|
DoD:
|
||||||
- repeated O(n) filtering in hot render paths is removed
|
- UI modules expose stable hooks for diagnostics and targeted testing where the current DOM is ambiguous
|
||||||
- tab counts and similar derived UI metrics use cached/derived snapshots instead of recomputing the same filters
|
- console/runtime failures on critical pages are easier to attribute to a concrete page module
|
||||||
- existing UI e2e coverage stays green after the performance polish
|
- existing UI e2e coverage stays green after the observability/testability pass
|
||||||
|
|
||||||
## Next
|
## Next
|
||||||
|
|
||||||
- `feat/frontend-observability-and-testability`
|
- `feat/id-display-support`
|
||||||
|
|
||||||
## Backlog
|
## Backlog
|
||||||
|
|
||||||
|
|||||||
+90
-10
@@ -109,6 +109,15 @@ document.addEventListener('alpine:init', function() {
|
|||||||
categoryOptions: [],
|
categoryOptions: [],
|
||||||
workspaceId: null,
|
workspaceId: null,
|
||||||
stats: emptyStats(),
|
stats: emptyStats(),
|
||||||
|
_opsVersion: 0,
|
||||||
|
_nonStatusViewCache: null,
|
||||||
|
_nonStatusViewKey: '',
|
||||||
|
_filteredCache: null,
|
||||||
|
_filteredKey: '',
|
||||||
|
_agentsByOpIdCache: null,
|
||||||
|
_agentsByOpIdCacheVersion: -1,
|
||||||
|
_activeAgentsCache: null,
|
||||||
|
_activeAgentsCacheVersion: -1,
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
var self = this;
|
var self = this;
|
||||||
@@ -150,13 +159,13 @@ document.addEventListener('alpine:init', function() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
var response = await window.CrankApi.listOperations(this.workspaceId);
|
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) {
|
this.categoryOptions = Array.from(new Set(this.operations.map(function(operation) {
|
||||||
return operation.category;
|
return operation.category;
|
||||||
}).filter(Boolean))).sort();
|
}).filter(Boolean))).sort();
|
||||||
this.stats = computeStats(this.operations);
|
this.stats = computeStats(this.operations);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.operations = [];
|
this.replaceOperations([]);
|
||||||
this.categoryOptions = [];
|
this.categoryOptions = [];
|
||||||
this.stats = emptyStats();
|
this.stats = emptyStats();
|
||||||
this.loadError = error.message || 'Failed to load operations';
|
this.loadError = error.message || 'Failed to load operations';
|
||||||
@@ -166,6 +175,19 @@ document.addEventListener('alpine:init', function() {
|
|||||||
this.page = 1;
|
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) {
|
_applyNonStatusFilters(operations) {
|
||||||
var filtered = operations;
|
var filtered = operations;
|
||||||
|
|
||||||
@@ -196,8 +218,55 @@ document.addEventListener('alpine:init', function() {
|
|||||||
return filtered;
|
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 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') {
|
if (this.tab !== 'all') {
|
||||||
operations = operations.filter(function(operation) {
|
operations = operations.filter(function(operation) {
|
||||||
@@ -213,7 +282,9 @@ document.addEventListener('alpine:init', function() {
|
|||||||
return 0;
|
return 0;
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
|
|
||||||
return operations;
|
this._filteredKey = key;
|
||||||
|
this._filteredCache = operations;
|
||||||
|
return this._filteredCache;
|
||||||
},
|
},
|
||||||
|
|
||||||
get totalFiltered() {
|
get totalFiltered() {
|
||||||
@@ -238,9 +309,8 @@ document.addEventListener('alpine:init', function() {
|
|||||||
},
|
},
|
||||||
|
|
||||||
tabCount(tab) {
|
tabCount(tab) {
|
||||||
var operations = this._applyNonStatusFilters(this.operations);
|
var tabCounts = this._nonStatusView().tabCounts;
|
||||||
if (tab === 'all') return operations.length;
|
return Object.prototype.hasOwnProperty.call(tabCounts, tab) ? tabCounts[tab] : 0;
|
||||||
return operations.filter(function(operation) { return operation.status === tab; }).length;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
get activeChips() {
|
get activeChips() {
|
||||||
@@ -275,14 +345,22 @@ document.addEventListener('alpine:init', function() {
|
|||||||
},
|
},
|
||||||
|
|
||||||
get agentsByOpId() {
|
get agentsByOpId() {
|
||||||
|
if (this._agentsByOpIdCache && this._agentsByOpIdCacheVersion === this._opsVersion) {
|
||||||
|
return this._agentsByOpIdCache;
|
||||||
|
}
|
||||||
var map = {};
|
var map = {};
|
||||||
this.operations.forEach(function(operation) {
|
this.operations.forEach(function(operation) {
|
||||||
map[operation.id] = operation.agent_refs || [];
|
map[operation.id] = operation.agent_refs || [];
|
||||||
});
|
});
|
||||||
return map;
|
this._agentsByOpIdCacheVersion = this._opsVersion;
|
||||||
|
this._agentsByOpIdCache = map;
|
||||||
|
return this._agentsByOpIdCache;
|
||||||
},
|
},
|
||||||
|
|
||||||
get activeAgents() {
|
get activeAgents() {
|
||||||
|
if (this._activeAgentsCache && this._activeAgentsCacheVersion === this._opsVersion) {
|
||||||
|
return this._activeAgentsCache;
|
||||||
|
}
|
||||||
var seen = {};
|
var seen = {};
|
||||||
var agents = [];
|
var agents = [];
|
||||||
this.operations.forEach(function(operation) {
|
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 left.display_name.localeCompare(right.display_name);
|
||||||
});
|
});
|
||||||
|
return this._activeAgentsCache;
|
||||||
},
|
},
|
||||||
|
|
||||||
get sortLabel() {
|
get sortLabel() {
|
||||||
@@ -371,7 +451,7 @@ document.addEventListener('alpine:init', function() {
|
|||||||
try {
|
try {
|
||||||
var operation = this.operations.find(function(item) { return item.id === id; });
|
var operation = this.operations.find(function(item) { return item.id === id; });
|
||||||
await window.CrankApi.deleteOperation(this.workspaceId, 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) {
|
this.categoryOptions = Array.from(new Set(this.operations.map(function(operation) {
|
||||||
return operation.category;
|
return operation.category;
|
||||||
}).filter(Boolean))).sort();
|
}).filter(Boolean))).sort();
|
||||||
|
|||||||
Reference in New Issue
Block a user