393 lines
12 KiB
JavaScript
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');
|
|
}
|
|
}
|