feature/refactoring #3
@@ -51,92 +51,56 @@ class ApiClient {
|
||||
}
|
||||
|
||||
async getClusterMembers() {
|
||||
try {
|
||||
return await this.request('/api/cluster/members', { method: 'GET' });
|
||||
} catch (error) {
|
||||
throw new Error(`Request failed: ${error.message}`);
|
||||
}
|
||||
return this.request('/api/cluster/members', { method: 'GET' });
|
||||
}
|
||||
|
||||
async getClusterMembersFromNode(ip) {
|
||||
try {
|
||||
return await this.request(`/api/cluster/members`, {
|
||||
method: 'GET',
|
||||
query: { ip: ip }
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Request failed: ${error.message}`);
|
||||
}
|
||||
return this.request(`/api/cluster/members`, {
|
||||
method: 'GET',
|
||||
query: { ip: ip }
|
||||
});
|
||||
}
|
||||
|
||||
async getDiscoveryInfo() {
|
||||
try {
|
||||
return await this.request('/api/discovery/nodes', { method: 'GET' });
|
||||
} catch (error) {
|
||||
throw new Error(`Request failed: ${error.message}`);
|
||||
}
|
||||
return this.request('/api/discovery/nodes', { method: 'GET' });
|
||||
}
|
||||
|
||||
async selectRandomPrimaryNode() {
|
||||
try {
|
||||
return await this.request('/api/discovery/random-primary', {
|
||||
method: 'POST',
|
||||
body: { timestamp: new Date().toISOString() }
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Request failed: ${error.message}`);
|
||||
}
|
||||
return this.request('/api/discovery/random-primary', {
|
||||
method: 'POST',
|
||||
body: { timestamp: new Date().toISOString() }
|
||||
});
|
||||
}
|
||||
|
||||
async getNodeStatus(ip) {
|
||||
try {
|
||||
return await this.request(`/api/node/status/${encodeURIComponent(ip)}`, { method: 'GET' });
|
||||
} catch (error) {
|
||||
throw new Error(`Request failed: ${error.message}`);
|
||||
}
|
||||
return this.request(`/api/node/status/${encodeURIComponent(ip)}`, { method: 'GET' });
|
||||
}
|
||||
|
||||
async getTasksStatus(ip) {
|
||||
try {
|
||||
return await this.request('/api/tasks/status', { method: 'GET', query: ip ? { ip } : undefined });
|
||||
} catch (error) {
|
||||
throw new Error(`Request failed: ${error.message}`);
|
||||
}
|
||||
return this.request('/api/tasks/status', { method: 'GET', query: ip ? { ip } : undefined });
|
||||
}
|
||||
|
||||
async getCapabilities(ip) {
|
||||
try {
|
||||
return await this.request('/api/capabilities', { method: 'GET', query: ip ? { ip } : undefined });
|
||||
} catch (error) {
|
||||
throw new Error(`Request failed: ${error.message}`);
|
||||
}
|
||||
return this.request('/api/capabilities', { method: 'GET', query: ip ? { ip } : undefined });
|
||||
}
|
||||
|
||||
async callCapability({ ip, method, uri, params }) {
|
||||
try {
|
||||
return await this.request('/api/proxy-call', {
|
||||
method: 'POST',
|
||||
body: { ip, method, uri, params }
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Request failed: ${error.message}`);
|
||||
}
|
||||
return this.request('/api/proxy-call', {
|
||||
method: 'POST',
|
||||
body: { ip, method, uri, params }
|
||||
});
|
||||
}
|
||||
|
||||
async uploadFirmware(file, nodeIp) {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
return await this.request(`/api/node/update`, {
|
||||
method: 'POST',
|
||||
query: { ip: nodeIp },
|
||||
body: formData,
|
||||
isForm: true,
|
||||
headers: {},
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Upload failed: ${error.message}`);
|
||||
}
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
return this.request(`/api/node/update`, {
|
||||
method: 'POST',
|
||||
query: { ip: nodeIp },
|
||||
body: formData,
|
||||
isForm: true,
|
||||
headers: {},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -285,22 +285,11 @@ class ClusterMembersComponent extends Component {
|
||||
|
||||
// Check if we should skip rendering during view switches
|
||||
shouldSkipRender() {
|
||||
// Skip rendering if we're in the middle of a view switch
|
||||
const isViewSwitching = document.querySelectorAll('.view-content.active').length === 0;
|
||||
if (isViewSwitching) {
|
||||
console.log('ClusterMembersComponent: View switching in progress, skipping render');
|
||||
// Rely on lifecycle flags controlled by App
|
||||
if (!this.isMounted || this.isPaused) {
|
||||
logger.debug('ClusterMembersComponent: Not mounted or paused, skipping render');
|
||||
return true;
|
||||
}
|
||||
|
||||
// Skip rendering if the component is not visible
|
||||
const isVisible = this.container.style.display !== 'none' &&
|
||||
this.container.style.opacity !== '0' &&
|
||||
this.container.classList.contains('active');
|
||||
if (!isVisible) {
|
||||
console.log('ClusterMembersComponent: Component not visible, skipping render');
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -308,11 +297,12 @@ class ClusterMembersComponent extends Component {
|
||||
updateMembersPartially(newMembers, previousMembers) {
|
||||
console.log('ClusterMembersComponent: Performing partial update to preserve UI state');
|
||||
|
||||
// Update only the data that changed, preserving expanded states and active tabs
|
||||
newMembers.forEach((newMember, index) => {
|
||||
const prevMember = previousMembers[index];
|
||||
// Build previous map by IP for stable diffs
|
||||
const prevByIp = new Map((previousMembers || []).map(m => [m.ip, m]));
|
||||
newMembers.forEach((newMember) => {
|
||||
const prevMember = prevByIp.get(newMember.ip);
|
||||
if (prevMember && this.hasMemberChanged(newMember, prevMember)) {
|
||||
this.updateMemberCard(newMember, index);
|
||||
this.updateMemberCard(newMember);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -325,7 +315,7 @@ class ClusterMembersComponent extends Component {
|
||||
}
|
||||
|
||||
// Update a specific member card without re-rendering the entire component
|
||||
updateMemberCard(member, index) {
|
||||
updateMemberCard(member) {
|
||||
const card = this.findElement(`[data-member-ip="${member.ip}"]`);
|
||||
if (!card) return;
|
||||
|
||||
@@ -451,9 +441,9 @@ class ClusterMembersComponent extends Component {
|
||||
<div class="member-status ${statusClass}">
|
||||
${statusIcon}
|
||||
</div>
|
||||
<div class="member-hostname">${member.hostname || 'Unknown Device'}</div>
|
||||
<div class="member-hostname">${this.escapeHtml(member.hostname || 'Unknown Device')}</div>
|
||||
</div>
|
||||
<div class="member-ip">${member.ip || 'No IP'}</div>
|
||||
<div class="member-ip">${this.escapeHtml(member.ip || 'No IP')}</div>
|
||||
<div class="member-latency">
|
||||
<span class="latency-label">Latency:</span>
|
||||
<span class="latency-value">${member.latency ? member.latency + 'ms' : 'N/A'}</span>
|
||||
@@ -462,7 +452,7 @@ class ClusterMembersComponent extends Component {
|
||||
${member.labels && Object.keys(member.labels).length ? `
|
||||
<div class="member-row-2">
|
||||
<div class="member-labels">
|
||||
${Object.entries(member.labels).map(([key, value]) => `<span class=\"label-chip\">${key}: ${value}</span>`).join('')}
|
||||
${Object.entries(member.labels).map(([key, value]) => `<span class=\"label-chip\">${this.escapeHtml(key)}: ${this.escapeHtml(value)}</span>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
@@ -807,8 +797,8 @@ class NodeDetailsComponent extends Component {
|
||||
}
|
||||
|
||||
renderNodeDetails(nodeStatus, tasks, capabilities) {
|
||||
// Always start with 'status' tab, don't restore previous state
|
||||
const activeTab = 'status';
|
||||
// Use persisted active tab from the view model, default to 'status'
|
||||
const activeTab = (this.viewModel && typeof this.viewModel.get === 'function' && this.viewModel.get('activeTab')) || 'status';
|
||||
console.log('NodeDetailsComponent: Rendering with activeTab:', activeTab);
|
||||
|
||||
const html = `
|
||||
|
||||
@@ -550,7 +550,7 @@ class Component {
|
||||
}
|
||||
|
||||
renderError(message) {
|
||||
const safe = String(message || 'An error occurred');
|
||||
const safe = this.escapeHtml(String(message || 'An error occurred'));
|
||||
const html = `
|
||||
<div class="error">
|
||||
<strong>Error:</strong><br>
|
||||
@@ -569,6 +569,16 @@ class Component {
|
||||
this.setHTML('', html);
|
||||
}
|
||||
|
||||
// Basic HTML escaping for dynamic values
|
||||
escapeHtml(str) {
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
// Tab helpers
|
||||
setupTabs(container = this.container, options = {}) {
|
||||
const { onChange } = options;
|
||||
|
||||
Reference in New Issue
Block a user