Files
spore-ui/public/view-models.js
2025-08-28 10:21:14 +02:00

393 lines
12 KiB
JavaScript

// View Models for SPORE UI Components
// Cluster View Model with enhanced state preservation
class ClusterViewModel extends ViewModel {
constructor() {
super();
this.setMultiple({
members: [],
primaryNode: null,
totalNodes: 0,
clientInitialized: false,
isLoading: false,
error: null,
expandedCards: new Map(),
activeTabs: new Map(), // Store active tab for each node
lastUpdateTime: null
});
// Initialize cluster status after a short delay to allow components to subscribe
setTimeout(() => {
this.updatePrimaryNodeDisplay();
}, 100);
}
// Update cluster members with state preservation
async updateClusterMembers() {
try {
console.log('ClusterViewModel: updateClusterMembers called');
// Store current UI state before update
const currentUIState = this.getAllUIState();
const currentExpandedCards = this.get('expandedCards');
const currentActiveTabs = this.get('activeTabs');
this.set('isLoading', true);
this.set('error', null);
console.log('ClusterViewModel: Fetching cluster members...');
const response = await window.apiClient.getClusterMembers();
console.log('ClusterViewModel: Got response:', response);
// Use batch update to preserve UI state
this.batchUpdate({
members: response.members || [],
lastUpdateTime: new Date().toISOString()
}, { preserveUIState: true });
// Restore expanded cards and active tabs
this.set('expandedCards', currentExpandedCards);
this.set('activeTabs', currentActiveTabs);
// Update primary node display
console.log('ClusterViewModel: Updating primary node display...');
await this.updatePrimaryNodeDisplay();
} catch (error) {
console.error('ClusterViewModel: Failed to fetch cluster members:', error);
this.set('error', error.message);
} finally {
this.set('isLoading', false);
console.log('ClusterViewModel: updateClusterMembers completed');
}
}
// Update primary node display with state preservation
async updatePrimaryNodeDisplay() {
try {
const discoveryInfo = await window.apiClient.getDiscoveryInfo();
// Use batch update to preserve UI state
const updates = {};
if (discoveryInfo.primaryNode) {
updates.primaryNode = discoveryInfo.primaryNode;
updates.clientInitialized = discoveryInfo.clientInitialized;
updates.totalNodes = discoveryInfo.totalNodes;
} else if (discoveryInfo.totalNodes > 0) {
updates.primaryNode = discoveryInfo.nodes[0]?.ip;
updates.clientInitialized = false;
updates.totalNodes = discoveryInfo.totalNodes;
} else {
updates.primaryNode = null;
updates.clientInitialized = false;
updates.totalNodes = 0;
}
this.batchUpdate(updates, { preserveUIState: true });
} catch (error) {
console.error('Failed to fetch discovery info:', error);
this.set('error', error.message);
}
}
// Select random primary node
async selectRandomPrimaryNode() {
try {
const result = await window.apiClient.selectRandomPrimaryNode();
if (result.success) {
// Update the display after a short delay
setTimeout(() => {
this.updatePrimaryNodeDisplay();
}, 1500);
return result;
} else {
throw new Error(result.message || 'Random selection failed');
}
} catch (error) {
console.error('Failed to select random primary node:', error);
throw error;
}
}
// Store expanded card state
storeExpandedCard(memberIp, content) {
const expandedCards = this.get('expandedCards');
expandedCards.set(memberIp, content);
this.set('expandedCards', expandedCards);
// Also store in UI state for persistence
this.setUIState(`expanded_${memberIp}`, content);
}
// Get expanded card state
getExpandedCard(memberIp) {
const expandedCards = this.get('expandedCards');
return expandedCards.get(memberIp);
}
// Clear expanded card state
clearExpandedCard(memberIp) {
const expandedCards = this.get('expandedCards');
expandedCards.delete(memberIp);
this.set('expandedCards', expandedCards);
// Also clear from UI state
this.clearUIState(`expanded_${memberIp}`);
}
// Store active tab for a specific node
storeActiveTab(memberIp, tabName) {
const activeTabs = this.get('activeTabs');
activeTabs.set(memberIp, tabName);
this.set('activeTabs', activeTabs);
// Also store in UI state for persistence
this.setUIState(`activeTab_${memberIp}`, tabName);
}
// Get active tab for a specific node
getActiveTab(memberIp) {
const activeTabs = this.get('activeTabs');
return activeTabs.get(memberIp) || 'status'; // Default to 'status' tab
}
// Check if data has actually changed to avoid unnecessary updates
hasDataChanged(newData, dataType) {
const currentData = this.get(dataType);
if (Array.isArray(newData) && Array.isArray(currentData)) {
if (newData.length !== currentData.length) return true;
// Compare each member's key properties
return newData.some((newMember, index) => {
const currentMember = currentData[index];
return !currentMember ||
newMember.ip !== currentMember.ip ||
newMember.status !== currentMember.status ||
newMember.latency !== currentMember.latency;
});
}
return newData !== currentData;
}
// Smart update that only updates changed data
async smartUpdate() {
try {
console.log('ClusterViewModel: Performing smart update...');
// Fetch new data
const response = await window.apiClient.getClusterMembers();
const newMembers = response.members || [];
// Check if members data has actually changed
if (this.hasDataChanged(newMembers, 'members')) {
console.log('ClusterViewModel: Members data changed, updating...');
await this.updateClusterMembers();
} else {
console.log('ClusterViewModel: Members data unchanged, skipping update');
// Still update primary node display as it might have changed
await this.updatePrimaryNodeDisplay();
}
} catch (error) {
console.error('ClusterViewModel: Smart update failed:', error);
this.set('error', error.message);
}
}
}
// Node Details View Model with enhanced state preservation
class NodeDetailsViewModel extends ViewModel {
constructor() {
super();
this.setMultiple({
nodeStatus: null,
tasks: [],
isLoading: false,
error: null,
activeTab: 'status',
nodeIp: null
});
}
// Load node details with state preservation
async loadNodeDetails(ip) {
try {
// Store current UI state
const currentActiveTab = this.get('activeTab');
this.set('isLoading', true);
this.set('error', null);
this.set('nodeIp', ip);
const nodeStatus = await window.apiClient.getNodeStatus(ip);
// Use batch update to preserve UI state
this.batchUpdate({
nodeStatus: nodeStatus
}, { preserveUIState: true });
// Restore active tab
this.set('activeTab', currentActiveTab);
// Load tasks data
await this.loadTasksData();
} catch (error) {
console.error('Failed to load node details:', error);
this.set('error', error.message);
} finally {
this.set('isLoading', false);
}
}
// Load tasks data with state preservation
async loadTasksData() {
try {
const ip = this.get('nodeIp');
const response = await window.apiClient.getTasksStatus(ip);
this.set('tasks', response || []);
} catch (error) {
console.error('Failed to load tasks:', error);
this.set('tasks', []);
}
}
// Set active tab with state persistence
setActiveTab(tabName) {
console.log('NodeDetailsViewModel: Setting activeTab to:', tabName);
this.set('activeTab', tabName);
// Store in UI state for persistence
this.setUIState('activeTab', tabName);
}
// Upload firmware
async uploadFirmware(file, nodeIp) {
try {
const result = await window.apiClient.uploadFirmware(file, nodeIp);
return result;
} catch (error) {
console.error('Firmware upload failed:', error);
throw error;
}
}
}
// Firmware View Model
class FirmwareViewModel extends ViewModel {
constructor() {
super();
this.setMultiple({
selectedFile: null,
targetType: 'all',
specificNode: null,
availableNodes: [],
uploadProgress: null,
uploadResults: [],
isUploading: false
});
}
// Set selected file
setSelectedFile(file) {
this.set('selectedFile', file);
}
// Set target type
setTargetType(type) {
this.set('targetType', type);
}
// Set specific node
setSpecificNode(nodeIp) {
this.set('specificNode', nodeIp);
}
// Update available nodes
updateAvailableNodes(nodes) {
this.set('availableNodes', nodes);
}
// Start upload
startUpload() {
this.set('isUploading', true);
this.set('uploadProgress', {
current: 0,
total: 0,
status: 'Preparing...'
});
this.set('uploadResults', []);
}
// Update upload progress
updateUploadProgress(current, total, status) {
this.set('uploadProgress', {
current,
total,
status
});
}
// Add upload result
addUploadResult(result) {
const results = this.get('uploadResults');
results.push(result);
this.set('uploadResults', results);
}
// Complete upload
completeUpload() {
this.set('isUploading', false);
}
// Reset upload state
resetUploadState() {
this.set('selectedFile', null);
this.set('uploadProgress', null);
this.set('uploadResults', []);
this.set('isUploading', false);
}
// Check if deploy button should be enabled
isDeployEnabled() {
const hasFile = this.get('selectedFile') !== null;
const availableNodes = this.get('availableNodes');
const hasAvailableNodes = availableNodes && availableNodes.length > 0;
let isValidTarget = false;
if (this.get('targetType') === 'all') {
isValidTarget = hasAvailableNodes;
} else if (this.get('targetType') === 'specific') {
isValidTarget = hasAvailableNodes && this.get('specificNode');
}
return hasFile && isValidTarget && !this.get('isUploading');
}
}
// Navigation View Model
class NavigationViewModel extends ViewModel {
constructor() {
super();
this.setMultiple({
activeView: 'cluster',
views: ['cluster', 'firmware']
});
}
// Set active view
setActiveView(viewName) {
this.set('activeView', viewName);
}
// Get active view
getActiveView() {
return this.get('activeView');
}
}