feat: frontend optimization, refactoring
This commit is contained in:
@@ -2,23 +2,44 @@
|
|||||||
|
|
||||||
class ApiClient {
|
class ApiClient {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.baseUrl = 'http://localhost:3001'; // Backend server URL
|
this.baseUrl = (typeof window !== 'undefined' && window.API_BASE_URL) || 'http://localhost:3001'; // Backend server URL
|
||||||
|
}
|
||||||
|
|
||||||
|
async request(path, { method = 'GET', headers = {}, body = undefined, query = undefined, isForm = false } = {}) {
|
||||||
|
const url = new URL(`${this.baseUrl}${path}`);
|
||||||
|
if (query && typeof query === 'object') {
|
||||||
|
Object.entries(query).forEach(([k, v]) => {
|
||||||
|
if (v !== undefined && v !== null) url.searchParams.set(k, String(v));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const finalHeaders = { 'Accept': 'application/json', ...headers };
|
||||||
|
const options = { method, headers: finalHeaders };
|
||||||
|
if (body !== undefined) {
|
||||||
|
if (isForm) {
|
||||||
|
options.body = body;
|
||||||
|
} else {
|
||||||
|
options.headers['Content-Type'] = options.headers['Content-Type'] || 'application/json';
|
||||||
|
options.body = typeof body === 'string' ? body : JSON.stringify(body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const response = await fetch(url.toString(), options);
|
||||||
|
let data;
|
||||||
|
const text = await response.text();
|
||||||
|
try {
|
||||||
|
data = text ? JSON.parse(text) : null;
|
||||||
|
} catch (_) {
|
||||||
|
data = text; // Non-JSON payload
|
||||||
|
}
|
||||||
|
if (!response.ok) {
|
||||||
|
const message = (data && data.message) || `HTTP ${response.status}: ${response.statusText}`;
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getClusterMembers() {
|
async getClusterMembers() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${this.baseUrl}/api/cluster/members`, {
|
return await this.request('/api/cluster/members', { method: 'GET' });
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await response.json();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Request failed: ${error.message}`);
|
throw new Error(`Request failed: ${error.message}`);
|
||||||
}
|
}
|
||||||
@@ -26,18 +47,7 @@ class ApiClient {
|
|||||||
|
|
||||||
async getDiscoveryInfo() {
|
async getDiscoveryInfo() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${this.baseUrl}/api/discovery/nodes`, {
|
return await this.request('/api/discovery/nodes', { method: 'GET' });
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await response.json();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Request failed: ${error.message}`);
|
throw new Error(`Request failed: ${error.message}`);
|
||||||
}
|
}
|
||||||
@@ -45,22 +55,10 @@ class ApiClient {
|
|||||||
|
|
||||||
async selectRandomPrimaryNode() {
|
async selectRandomPrimaryNode() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${this.baseUrl}/api/discovery/random-primary`, {
|
return await this.request('/api/discovery/random-primary', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
body: { timestamp: new Date().toISOString() }
|
||||||
'Accept': 'application/json',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await response.json();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Request failed: ${error.message}`);
|
throw new Error(`Request failed: ${error.message}`);
|
||||||
}
|
}
|
||||||
@@ -68,18 +66,7 @@ class ApiClient {
|
|||||||
|
|
||||||
async getNodeStatus(ip) {
|
async getNodeStatus(ip) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${this.baseUrl}/api/node/status/${encodeURIComponent(ip)}`, {
|
return await this.request(`/api/node/status/${encodeURIComponent(ip)}`, { method: 'GET' });
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await response.json();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Request failed: ${error.message}`);
|
throw new Error(`Request failed: ${error.message}`);
|
||||||
}
|
}
|
||||||
@@ -87,21 +74,7 @@ class ApiClient {
|
|||||||
|
|
||||||
async getTasksStatus(ip) {
|
async getTasksStatus(ip) {
|
||||||
try {
|
try {
|
||||||
const url = ip
|
return await this.request('/api/tasks/status', { method: 'GET', query: ip ? { ip } : undefined });
|
||||||
? `${this.baseUrl}/api/tasks/status?ip=${encodeURIComponent(ip)}`
|
|
||||||
: `${this.baseUrl}/api/tasks/status`;
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await response.json();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Request failed: ${error.message}`);
|
throw new Error(`Request failed: ${error.message}`);
|
||||||
}
|
}
|
||||||
@@ -109,21 +82,7 @@ class ApiClient {
|
|||||||
|
|
||||||
async getCapabilities(ip) {
|
async getCapabilities(ip) {
|
||||||
try {
|
try {
|
||||||
const url = ip
|
return await this.request('/api/capabilities', { method: 'GET', query: ip ? { ip } : undefined });
|
||||||
? `${this.baseUrl}/api/capabilities?ip=${encodeURIComponent(ip)}`
|
|
||||||
: `${this.baseUrl}/api/capabilities`;
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await response.json();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Request failed: ${error.message}`);
|
throw new Error(`Request failed: ${error.message}`);
|
||||||
}
|
}
|
||||||
@@ -131,19 +90,10 @@ class ApiClient {
|
|||||||
|
|
||||||
async callCapability({ ip, method, uri, params }) {
|
async callCapability({ ip, method, uri, params }) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${this.baseUrl}/api/proxy-call`, {
|
return await this.request('/api/proxy-call', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
body: { ip, method, uri, params }
|
||||||
'Accept': 'application/json',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ ip, method, uri, params })
|
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
|
||||||
const errorData = await response.json().catch(() => ({}));
|
|
||||||
throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
return await response.json();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Request failed: ${error.message}`);
|
throw new Error(`Request failed: ${error.message}`);
|
||||||
}
|
}
|
||||||
@@ -153,18 +103,13 @@ class ApiClient {
|
|||||||
try {
|
try {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
|
return await this.request(`/api/node/update`, {
|
||||||
const response = await fetch(`${this.baseUrl}/api/node/update?ip=${encodeURIComponent(nodeIp)}`, {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData
|
query: { ip: nodeIp },
|
||||||
|
body: formData,
|
||||||
|
isForm: true,
|
||||||
|
headers: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorData = await response.json();
|
|
||||||
throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await response.json();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Upload failed: ${error.message}`);
|
throw new Error(`Upload failed: ${error.message}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ class ClusterMembersComponent extends Component {
|
|||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
console.log('ClusterMembersComponent: Showing loading state');
|
console.log('ClusterMembersComponent: Showing loading state');
|
||||||
this.showLoadingState();
|
this.renderLoading(`\n <div class="loading">\n <div>Loading cluster members...</div>\n </div>\n `);
|
||||||
|
|
||||||
// Set up a loading completion check
|
// Set up a loading completion check
|
||||||
this.checkLoadingCompletion();
|
this.checkLoadingCompletion();
|
||||||
@@ -402,7 +402,7 @@ class ClusterMembersComponent extends Component {
|
|||||||
// Show loading state
|
// Show loading state
|
||||||
showLoadingState() {
|
showLoadingState() {
|
||||||
console.log('ClusterMembersComponent: showLoadingState() called');
|
console.log('ClusterMembersComponent: showLoadingState() called');
|
||||||
this.setHTML('', `
|
this.renderLoading(`
|
||||||
<div class="loading">
|
<div class="loading">
|
||||||
<div>Loading cluster members...</div>
|
<div>Loading cluster members...</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -412,18 +412,13 @@ class ClusterMembersComponent extends Component {
|
|||||||
// Show error state
|
// Show error state
|
||||||
showErrorState(error) {
|
showErrorState(error) {
|
||||||
console.log('ClusterMembersComponent: showErrorState() called with error:', error);
|
console.log('ClusterMembersComponent: showErrorState() called with error:', error);
|
||||||
this.setHTML('', `
|
this.renderError(`Error loading cluster members: ${error}`);
|
||||||
<div class="error">
|
|
||||||
<strong>Error loading cluster members:</strong><br>
|
|
||||||
${error}
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show empty state
|
// Show empty state
|
||||||
showEmptyState() {
|
showEmptyState() {
|
||||||
console.log('ClusterMembersComponent: showEmptyState() called');
|
console.log('ClusterMembersComponent: showEmptyState() called');
|
||||||
this.setHTML('', `
|
this.renderEmpty(`
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<div class="empty-state-icon">🌐</div>
|
<div class="empty-state-icon">🌐</div>
|
||||||
<div>No cluster members found</div>
|
<div>No cluster members found</div>
|
||||||
@@ -571,16 +566,8 @@ class ClusterMembersComponent extends Component {
|
|||||||
|
|
||||||
const targetTab = button.dataset.tab;
|
const targetTab = button.dataset.tab;
|
||||||
|
|
||||||
// Remove active class from all buttons and contents
|
// Use base helper to set active tab
|
||||||
tabButtons.forEach(btn => btn.classList.remove('active'));
|
this.setActiveTab(targetTab, container);
|
||||||
tabContents.forEach(content => content.classList.remove('active'));
|
|
||||||
|
|
||||||
// Add active class to clicked button and corresponding content
|
|
||||||
button.classList.add('active');
|
|
||||||
const targetContent = container.querySelector(`#${targetTab}-tab`);
|
|
||||||
if (targetContent) {
|
|
||||||
targetContent.classList.add('active');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store active tab state
|
// Store active tab state
|
||||||
const memberCard = container.closest('.member-card');
|
const memberCard = container.closest('.member-card');
|
||||||
@@ -772,19 +759,14 @@ class NodeDetailsComponent extends Component {
|
|||||||
// Handle loading state update
|
// Handle loading state update
|
||||||
handleLoadingUpdate(isLoading) {
|
handleLoadingUpdate(isLoading) {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
this.setHTML('', '<div class="loading-details">Loading detailed information...</div>');
|
this.renderLoading('<div class="loading-details">Loading detailed information...</div>');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle error state update
|
// Handle error state update
|
||||||
handleErrorUpdate(error) {
|
handleErrorUpdate(error) {
|
||||||
if (error) {
|
if (error) {
|
||||||
this.setHTML('', `
|
this.renderError(`Error loading node details: ${error}`);
|
||||||
<div class="error">
|
|
||||||
<strong>Error loading node details:</strong><br>
|
|
||||||
${error}
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -811,22 +793,17 @@ class NodeDetailsComponent extends Component {
|
|||||||
const capabilities = this.viewModel.get('capabilities');
|
const capabilities = this.viewModel.get('capabilities');
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
this.setHTML('', '<div class="loading-details">Loading detailed information...</div>');
|
this.renderLoading('<div class="loading-details">Loading detailed information...</div>');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
this.setHTML('', `
|
this.renderError(`Error loading node details: ${error}`);
|
||||||
<div class="error">
|
|
||||||
<strong>Error loading node details:</strong><br>
|
|
||||||
${error}
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!nodeStatus) {
|
if (!nodeStatus) {
|
||||||
this.setHTML('', '<div class="loading-details">No node status available</div>');
|
this.renderEmpty('<div class="loading-details">No node status available</div>');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1036,7 +1013,14 @@ class NodeDetailsComponent extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderTasksTab(tasks) {
|
renderTasksTab(tasks) {
|
||||||
|
const summary = this.viewModel.get('tasksSummary');
|
||||||
if (tasks && tasks.length > 0) {
|
if (tasks && tasks.length > 0) {
|
||||||
|
const summaryHTML = summary ? `
|
||||||
|
<div class="tasks-summary">
|
||||||
|
<span>Total: ${summary.totalTasks ?? tasks.length}</span>
|
||||||
|
<span style="margin-left: 0.75rem;">Active: ${summary.activeTasks ?? tasks.filter(t => t.running).length}</span>
|
||||||
|
</div>
|
||||||
|
` : '';
|
||||||
const tasksHTML = tasks.map(task => `
|
const tasksHTML = tasks.map(task => `
|
||||||
<div class="task-item">
|
<div class="task-item">
|
||||||
<div class="task-header">
|
<div class="task-header">
|
||||||
@@ -1053,14 +1037,17 @@ class NodeDetailsComponent extends Component {
|
|||||||
`).join('');
|
`).join('');
|
||||||
|
|
||||||
return `
|
return `
|
||||||
|
${summaryHTML}
|
||||||
${tasksHTML}
|
${tasksHTML}
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
|
const total = summary?.totalTasks ?? 0;
|
||||||
|
const active = summary?.activeTasks ?? 0;
|
||||||
return `
|
return `
|
||||||
<div class="no-tasks">
|
<div class="no-tasks">
|
||||||
<div>📋 No active tasks found</div>
|
<div>📋 No active tasks found</div>
|
||||||
<div style="font-size: 0.9rem; margin-top: 0.5rem; opacity: 0.7;">
|
<div style="font-size: 0.9rem; margin-top: 0.5rem; opacity: 0.7;">
|
||||||
This node has no running tasks
|
${total > 0 ? `Total tasks: ${total}, active: ${active}` : 'This node has no running tasks'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -1096,7 +1083,7 @@ class NodeDetailsComponent extends Component {
|
|||||||
console.log('NodeDetailsComponent: Tab clicked:', targetTab);
|
console.log('NodeDetailsComponent: Tab clicked:', targetTab);
|
||||||
|
|
||||||
// Update tab UI locally, don't store in view model
|
// Update tab UI locally, don't store in view model
|
||||||
this.updateActiveTab(targetTab);
|
this.setActiveTab(targetTab);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1110,20 +1097,7 @@ class NodeDetailsComponent extends Component {
|
|||||||
|
|
||||||
// Update active tab without full re-render
|
// Update active tab without full re-render
|
||||||
updateActiveTab(newTab, previousTab = null) {
|
updateActiveTab(newTab, previousTab = null) {
|
||||||
const tabButtons = this.findAllElements('.tab-button');
|
this.setActiveTab(newTab);
|
||||||
const tabContents = this.findAllElements('.tab-content');
|
|
||||||
|
|
||||||
// Remove active class from all buttons and contents
|
|
||||||
tabButtons.forEach(btn => btn.classList.remove('active'));
|
|
||||||
tabContents.forEach(content => content.classList.remove('active'));
|
|
||||||
|
|
||||||
// Add active class to new active tab
|
|
||||||
const activeButton = this.findElement(`[data-tab="${newTab}"]`);
|
|
||||||
const activeContent = this.findElement(`#${newTab}-tab`);
|
|
||||||
|
|
||||||
if (activeButton) activeButton.classList.add('active');
|
|
||||||
if (activeContent) activeContent.classList.add('active');
|
|
||||||
|
|
||||||
console.log(`NodeDetailsComponent: Active tab updated to '${newTab}'`);
|
console.log(`NodeDetailsComponent: Active tab updated to '${newTab}'`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
// SPORE UI Framework - Component-based architecture with pub/sub system
|
// SPORE UI Framework - Component-based architecture with pub/sub system
|
||||||
|
|
||||||
|
// Lightweight logger with level gating
|
||||||
|
const logger = {
|
||||||
|
debug: (...args) => { try { if (window && window.DEBUG) { console.debug(...args); } } catch (_) { /* no-op */ } },
|
||||||
|
info: (...args) => console.info(...args),
|
||||||
|
warn: (...args) => console.warn(...args),
|
||||||
|
error: (...args) => console.error(...args),
|
||||||
|
};
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.logger = window.logger || logger;
|
||||||
|
}
|
||||||
|
|
||||||
// Event Bus for pub/sub communication
|
// Event Bus for pub/sub communication
|
||||||
class EventBus {
|
class EventBus {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -99,24 +110,25 @@ class ViewModel {
|
|||||||
// Set multiple properties at once with change detection
|
// Set multiple properties at once with change detection
|
||||||
setMultiple(properties) {
|
setMultiple(properties) {
|
||||||
const changedProperties = {};
|
const changedProperties = {};
|
||||||
const unchangedProperties = {};
|
|
||||||
|
|
||||||
|
// Determine changes and update previousData snapshot per key
|
||||||
Object.keys(properties).forEach(key => {
|
Object.keys(properties).forEach(key => {
|
||||||
if (this._data[key] !== properties[key]) {
|
const newValue = properties[key];
|
||||||
changedProperties[key] = properties[key];
|
const oldValue = this._data[key];
|
||||||
} else {
|
if (oldValue !== newValue) {
|
||||||
unchangedProperties[key] = properties[key];
|
this._previousData[key] = oldValue;
|
||||||
|
changedProperties[key] = newValue;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set all properties
|
// Apply all properties
|
||||||
Object.keys(properties).forEach(key => {
|
Object.keys(properties).forEach(key => {
|
||||||
this._data[key] = properties[key];
|
this._data[key] = properties[key];
|
||||||
});
|
});
|
||||||
|
|
||||||
// Notify listeners only for changed properties
|
// Notify listeners only for changed properties with accurate previous values
|
||||||
Object.keys(changedProperties).forEach(key => {
|
Object.keys(changedProperties).forEach(key => {
|
||||||
this._notifyListeners(key, changedProperties[key], this._previousData[key]);
|
this._notifyListeners(key, this._data[key], this._previousData[key]);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (Object.keys(changedProperties).length > 0) {
|
if (Object.keys(changedProperties).length > 0) {
|
||||||
@@ -215,28 +227,33 @@ class ViewModel {
|
|||||||
batchUpdate(updates, options = {}) {
|
batchUpdate(updates, options = {}) {
|
||||||
const { preserveUIState = true, notifyChanges = true } = options;
|
const { preserveUIState = true, notifyChanges = true } = options;
|
||||||
|
|
||||||
if (preserveUIState) {
|
// Optionally preserve UI state snapshot
|
||||||
// Store current UI state
|
const currentUIState = preserveUIState ? new Map(this._uiState) : null;
|
||||||
const currentUIState = new Map(this._uiState);
|
|
||||||
|
|
||||||
// Apply updates
|
// Track which keys actually change and what the previous values were
|
||||||
|
const changedKeys = [];
|
||||||
Object.keys(updates).forEach(key => {
|
Object.keys(updates).forEach(key => {
|
||||||
this._data[key] = updates[key];
|
const newValue = updates[key];
|
||||||
});
|
const oldValue = this._data[key];
|
||||||
|
if (oldValue !== newValue) {
|
||||||
// Restore UI state
|
this._previousData[key] = oldValue;
|
||||||
this._uiState = currentUIState;
|
this._data[key] = newValue;
|
||||||
|
changedKeys.push(key);
|
||||||
} else {
|
} else {
|
||||||
// Apply updates normally
|
// Still apply to ensure consistency if needed
|
||||||
Object.keys(updates).forEach(key => {
|
this._data[key] = newValue;
|
||||||
this._data[key] = updates[key];
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Restore UI state if requested
|
||||||
|
if (preserveUIState && currentUIState) {
|
||||||
|
this._uiState = currentUIState;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify listeners if requested
|
// Notify listeners for changed keys
|
||||||
if (notifyChanges) {
|
if (notifyChanges) {
|
||||||
Object.keys(updates).forEach(key => {
|
changedKeys.forEach(key => {
|
||||||
this._notifyListeners(key, updates[key], this._previousData[key]);
|
this._notifyListeners(key, this._data[key], this._previousData[key]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -521,6 +538,66 @@ class Component {
|
|||||||
element.disabled = !enabled;
|
element.disabled = !enabled;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reusable render helpers
|
||||||
|
renderLoading(customHtml) {
|
||||||
|
const html = customHtml || `
|
||||||
|
<div class="loading">
|
||||||
|
<div>Loading...</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
this.setHTML('', html);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderError(message) {
|
||||||
|
const safe = String(message || 'An error occurred');
|
||||||
|
const html = `
|
||||||
|
<div class="error">
|
||||||
|
<strong>Error:</strong><br>
|
||||||
|
${safe}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
this.setHTML('', html);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderEmpty(customHtml) {
|
||||||
|
const html = customHtml || `
|
||||||
|
<div class="empty-state">
|
||||||
|
<div>No data</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
this.setHTML('', html);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab helpers
|
||||||
|
setupTabs(container = this.container) {
|
||||||
|
const tabButtons = container.querySelectorAll('.tab-button');
|
||||||
|
const tabContents = container.querySelectorAll('.tab-content');
|
||||||
|
tabButtons.forEach(button => {
|
||||||
|
this.addEventListener(button, 'click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const targetTab = button.dataset.tab;
|
||||||
|
this.setActiveTab(targetTab, container);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
tabContents.forEach(content => {
|
||||||
|
this.addEventListener(content, 'click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setActiveTab(tabName, container = this.container) {
|
||||||
|
const tabButtons = container.querySelectorAll('.tab-button');
|
||||||
|
const tabContents = container.querySelectorAll('.tab-content');
|
||||||
|
tabButtons.forEach(btn => btn.classList.remove('active'));
|
||||||
|
tabContents.forEach(content => content.classList.remove('active'));
|
||||||
|
const activeButton = container.querySelector(`[data-tab="${tabName}"]`);
|
||||||
|
const activeContent = container.querySelector(`#${tabName}-tab`);
|
||||||
|
if (activeButton) activeButton.classList.add('active');
|
||||||
|
if (activeContent) activeContent.classList.add('active');
|
||||||
|
logger.debug(`${this.constructor.name}: Active tab set to '${tabName}'`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Application class to manage components and routing
|
// Application class to manage components and routing
|
||||||
|
|||||||
@@ -212,7 +212,8 @@ class NodeDetailsViewModel extends ViewModel {
|
|||||||
error: null,
|
error: null,
|
||||||
activeTab: 'status',
|
activeTab: 'status',
|
||||||
nodeIp: null,
|
nodeIp: null,
|
||||||
capabilities: null
|
capabilities: null,
|
||||||
|
tasksSummary: null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,10 +256,12 @@ class NodeDetailsViewModel extends ViewModel {
|
|||||||
try {
|
try {
|
||||||
const ip = this.get('nodeIp');
|
const ip = this.get('nodeIp');
|
||||||
const response = await window.apiClient.getTasksStatus(ip);
|
const response = await window.apiClient.getTasksStatus(ip);
|
||||||
this.set('tasks', response || []);
|
this.set('tasks', (response && Array.isArray(response.tasks)) ? response.tasks : []);
|
||||||
|
this.set('tasksSummary', response && response.summary ? response.summary : null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load tasks:', error);
|
console.error('Failed to load tasks:', error);
|
||||||
this.set('tasks', []);
|
this.set('tasks', []);
|
||||||
|
this.set('tasksSummary', null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user