feat: implement framework and refactor everything
This commit is contained in:
387
public/view-models.js
Normal file
387
public/view-models.js
Normal file
@@ -0,0 +1,387 @@
|
||||
// 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
|
||||
});
|
||||
}
|
||||
|
||||
// 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 response = await window.apiClient.getTasksStatus();
|
||||
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');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user