chore: restructure public files
This commit is contained in:
632
public/scripts/view-models.js
Normal file
632
public/scripts/view-models.js
Normal file
@@ -0,0 +1,632 @@
|
||||
// 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,
|
||||
onlineNodes: 0
|
||||
});
|
||||
|
||||
// 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);
|
||||
|
||||
const members = response.members || [];
|
||||
const onlineNodes = Array.isArray(members)
|
||||
? members.filter(m => m && m.status === 'active').length
|
||||
: 0;
|
||||
|
||||
// Use batch update to preserve UI state
|
||||
this.batchUpdate({
|
||||
members: members,
|
||||
lastUpdateTime: new Date().toISOString(),
|
||||
onlineNodes: onlineNodes
|
||||
}, { 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,
|
||||
capabilities: null,
|
||||
tasksSummary: 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();
|
||||
|
||||
// Load capabilities data
|
||||
await this.loadCapabilitiesData();
|
||||
|
||||
} 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 && Array.isArray(response.tasks)) ? response.tasks : []);
|
||||
this.set('tasksSummary', response && response.summary ? response.summary : null);
|
||||
} catch (error) {
|
||||
console.error('Failed to load tasks:', error);
|
||||
this.set('tasks', []);
|
||||
this.set('tasksSummary', null);
|
||||
}
|
||||
}
|
||||
|
||||
// Load capabilities data with state preservation
|
||||
async loadCapabilitiesData() {
|
||||
try {
|
||||
const ip = this.get('nodeIp');
|
||||
const response = await window.apiClient.getCapabilities(ip);
|
||||
this.set('capabilities', response || null);
|
||||
} catch (error) {
|
||||
console.error('Failed to load capabilities:', error);
|
||||
this.set('capabilities', null);
|
||||
}
|
||||
}
|
||||
|
||||
// Invoke a capability against this node
|
||||
async callCapability(method, uri, params) {
|
||||
const ip = this.get('nodeIp');
|
||||
return window.apiClient.callCapability({ ip, method, uri, params });
|
||||
}
|
||||
|
||||
// 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,
|
||||
selectedLabels: [],
|
||||
availableLabels: []
|
||||
});
|
||||
}
|
||||
|
||||
// Set selected file
|
||||
setSelectedFile(file) {
|
||||
this.set('selectedFile', file);
|
||||
}
|
||||
|
||||
// Set target type
|
||||
setTargetType(type) {
|
||||
this.set('targetType', type);
|
||||
// Clear any previously selected labels when switching options
|
||||
const currentLabels = this.get('selectedLabels') || [];
|
||||
if (currentLabels.length > 0) {
|
||||
this.set('selectedLabels', []);
|
||||
}
|
||||
}
|
||||
|
||||
// Set specific node
|
||||
setSpecificNode(nodeIp) {
|
||||
this.set('specificNode', nodeIp);
|
||||
}
|
||||
|
||||
// Update available nodes
|
||||
updateAvailableNodes(nodes) {
|
||||
this.set('availableNodes', nodes);
|
||||
// Compute availableLabels as unique key=value pairs from nodes' labels
|
||||
try {
|
||||
const labelSet = new Set();
|
||||
(nodes || []).forEach(n => {
|
||||
const labels = n && n.labels ? n.labels : {};
|
||||
Object.entries(labels).forEach(([k, v]) => {
|
||||
labelSet.add(`${k}=${v}`);
|
||||
});
|
||||
});
|
||||
const availableLabels = Array.from(labelSet).sort((a, b) => a.localeCompare(b));
|
||||
this.set('availableLabels', availableLabels);
|
||||
// Prune selected labels that are no longer available
|
||||
const selected = this.get('selectedLabels') || [];
|
||||
const pruned = selected.filter(x => availableLabels.includes(x));
|
||||
if (pruned.length !== selected.length) {
|
||||
this.set('selectedLabels', pruned);
|
||||
}
|
||||
} catch (_) {
|
||||
this.set('availableLabels', []);
|
||||
this.set('selectedLabels', []);
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Set selected labels
|
||||
setSelectedLabels(labels) {
|
||||
this.set('selectedLabels', Array.isArray(labels) ? labels : []);
|
||||
}
|
||||
|
||||
// Return nodes matching ALL selected label pairs
|
||||
getAffectedNodesByLabels() {
|
||||
const selected = this.get('selectedLabels') || [];
|
||||
if (!selected.length) return [];
|
||||
const selectedPairs = selected.map(s => {
|
||||
const idx = String(s).indexOf('=');
|
||||
return idx > -1 ? { key: s.slice(0, idx), value: s.slice(idx + 1) } : null;
|
||||
}).filter(Boolean);
|
||||
const nodes = this.get('availableNodes') || [];
|
||||
return nodes.filter(n => {
|
||||
const labels = n && n.labels ? n.labels : {};
|
||||
return selectedPairs.every(p => String(labels[p.key]) === String(p.value));
|
||||
});
|
||||
}
|
||||
|
||||
// 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');
|
||||
} else if (this.get('targetType') === 'labels') {
|
||||
const affected = this.getAffectedNodesByLabels();
|
||||
isValidTarget = hasAvailableNodes && (this.get('selectedLabels') || []).length > 0 && affected.length > 0;
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
// Topology View Model for network topology visualization
|
||||
class TopologyViewModel extends ViewModel {
|
||||
constructor() {
|
||||
super();
|
||||
this.setMultiple({
|
||||
nodes: [],
|
||||
links: [],
|
||||
isLoading: false,
|
||||
error: null,
|
||||
lastUpdateTime: null,
|
||||
selectedNode: null
|
||||
});
|
||||
}
|
||||
|
||||
// Update network topology data
|
||||
async updateNetworkTopology() {
|
||||
try {
|
||||
console.log('TopologyViewModel: updateNetworkTopology called');
|
||||
|
||||
this.set('isLoading', true);
|
||||
this.set('error', null);
|
||||
|
||||
// Get cluster members from the primary node
|
||||
const response = await window.apiClient.getClusterMembers();
|
||||
console.log('TopologyViewModel: Got cluster members response:', response);
|
||||
|
||||
const members = response.members || [];
|
||||
|
||||
// Build enhanced graph data with actual node connections
|
||||
const { nodes, links } = await this.buildEnhancedGraphData(members);
|
||||
|
||||
this.batchUpdate({
|
||||
nodes: nodes,
|
||||
links: links,
|
||||
lastUpdateTime: new Date().toISOString()
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('TopologyViewModel: Failed to fetch network topology:', error);
|
||||
this.set('error', error.message);
|
||||
} finally {
|
||||
this.set('isLoading', false);
|
||||
console.log('TopologyViewModel: updateNetworkTopology completed');
|
||||
}
|
||||
}
|
||||
|
||||
// Build enhanced graph data with actual node connections
|
||||
async buildEnhancedGraphData(members) {
|
||||
const nodes = [];
|
||||
const links = [];
|
||||
const nodeConnections = new Map();
|
||||
|
||||
// Create nodes from members
|
||||
members.forEach((member, index) => {
|
||||
if (member && member.ip) {
|
||||
nodes.push({
|
||||
id: member.ip,
|
||||
hostname: member.hostname || member.ip,
|
||||
ip: member.ip,
|
||||
status: member.status || 'UNKNOWN',
|
||||
latency: member.latency || 0,
|
||||
resources: member.resources || {},
|
||||
x: Math.random() * 1200 + 100, // Better spacing for 1400px width
|
||||
y: Math.random() * 800 + 100 // Better spacing for 1000px height
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Try to get cluster members from each node to build actual connections
|
||||
for (const node of nodes) {
|
||||
try {
|
||||
const nodeResponse = await window.apiClient.getClusterMembersFromNode(node.ip);
|
||||
if (nodeResponse && nodeResponse.members) {
|
||||
nodeConnections.set(node.ip, nodeResponse.members);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to get cluster members from node ${node.ip}:`, error);
|
||||
// Continue with other nodes
|
||||
}
|
||||
}
|
||||
|
||||
// Build links based on actual connections
|
||||
for (const [sourceIp, sourceMembers] of nodeConnections) {
|
||||
for (const targetMember of sourceMembers) {
|
||||
if (targetMember.ip && targetMember.ip !== sourceIp) {
|
||||
// Check if we already have this link
|
||||
const existingLink = links.find(link =>
|
||||
(link.source === sourceIp && link.target === targetMember.ip) ||
|
||||
(link.source === targetMember.ip && link.target === sourceIp)
|
||||
);
|
||||
|
||||
if (!existingLink) {
|
||||
const sourceNode = nodes.find(n => n.id === sourceIp);
|
||||
const targetNode = nodes.find(n => n.id === targetMember.ip);
|
||||
|
||||
if (sourceNode && targetNode) {
|
||||
const latency = targetMember.latency || this.estimateLatency(sourceNode, targetNode);
|
||||
|
||||
links.push({
|
||||
source: sourceIp,
|
||||
target: targetMember.ip,
|
||||
latency: latency,
|
||||
sourceNode: sourceNode,
|
||||
targetNode: targetNode,
|
||||
bidirectional: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no actual connections found, create a basic mesh
|
||||
if (links.length === 0) {
|
||||
console.log('TopologyViewModel: No actual connections found, creating basic mesh');
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
for (let j = i + 1; j < nodes.length; j++) {
|
||||
const sourceNode = nodes[i];
|
||||
const targetNode = nodes[j];
|
||||
|
||||
const estimatedLatency = this.estimateLatency(sourceNode, targetNode);
|
||||
|
||||
links.push({
|
||||
source: sourceNode.id,
|
||||
target: targetNode.id,
|
||||
latency: estimatedLatency,
|
||||
sourceNode: sourceNode,
|
||||
targetNode: targetNode,
|
||||
bidirectional: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { nodes, links };
|
||||
}
|
||||
|
||||
// Estimate latency between two nodes
|
||||
estimateLatency(sourceNode, targetNode) {
|
||||
// Simple estimation - in a real implementation, you'd get actual measurements
|
||||
const baseLatency = 5; // Base latency in ms
|
||||
const randomVariation = Math.random() * 10; // Random variation
|
||||
return Math.round(baseLatency + randomVariation);
|
||||
}
|
||||
|
||||
// Select a node in the graph
|
||||
selectNode(nodeId) {
|
||||
this.set('selectedNode', nodeId);
|
||||
}
|
||||
|
||||
// Clear node selection
|
||||
clearSelection() {
|
||||
this.set('selectedNode', null);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user