`;
});
// Open drawer
this.detailsDrawer.classList.add('open');
this.detailsDrawerBackdrop.classList.add('visible');
}
closeDrawer() {
if (this.detailsDrawer) this.detailsDrawer.classList.remove('open');
if (this.detailsDrawerBackdrop) this.detailsDrawerBackdrop.classList.remove('visible');
}
mount() {
logger.debug('ClusterMembersComponent: Starting mount...');
super.mount();
// Show loading state immediately when mounted
logger.debug('ClusterMembersComponent: Showing initial loading state');
this.showLoadingState();
// Set up loading timeout safeguard
this.setupLoadingTimeout();
logger.debug('ClusterMembersComponent: Mounted successfully');
}
// Setup loading timeout safeguard to prevent getting stuck in loading state
setupLoadingTimeout() {
this.loadingTimeout = setTimeout(() => {
const isLoading = this.viewModel.get('isLoading');
if (isLoading) {
logger.warn('ClusterMembersComponent: Loading timeout reached, forcing render check');
this.forceRenderCheck();
}
}, 10000); // 10 second timeout
}
// Force a render check when loading gets stuck
forceRenderCheck() {
logger.debug('ClusterMembersComponent: Force render check called');
const members = this.viewModel.get('members');
const error = this.viewModel.get('error');
const isLoading = this.viewModel.get('isLoading');
logger.debug('ClusterMembersComponent: Force render check state:', { members, error, isLoading });
if (error) {
this.showErrorState(error);
} else if (members && members.length > 0) {
this.renderMembers(members);
} else if (!isLoading) {
this.showEmptyState();
}
}
setupEventListeners() {
logger.debug('ClusterMembersComponent: Setting up event listeners...');
// Note: Refresh button is now handled by ClusterViewComponent
// since it's in the cluster header, not in the members container
}
setupViewModelListeners() {
logger.debug('ClusterMembersComponent: Setting up view model listeners...');
// Listen to cluster members changes with change detection
this.subscribeToProperty('members', this.handleMembersUpdate.bind(this));
this.subscribeToProperty('isLoading', this.handleLoadingUpdate.bind(this));
this.subscribeToProperty('error', this.handleErrorUpdate.bind(this));
logger.debug('ClusterMembersComponent: View model listeners set up');
}
// Handle members update with state preservation
handleMembersUpdate(newMembers, previousMembers) {
logger.debug('ClusterMembersComponent: Members updated:', { newMembers, previousMembers });
// Prevent multiple simultaneous renders
if (this.renderInProgress) {
logger.debug('ClusterMembersComponent: Render already in progress, skipping update');
return;
}
// Check if we're currently loading - if so, let the loading handler deal with it
const isLoading = this.viewModel.get('isLoading');
if (isLoading) {
logger.debug('ClusterMembersComponent: Currently loading, skipping members update (will be handled by loading completion)');
return;
}
// On first load (no previous members), always render
if (!previousMembers || !Array.isArray(previousMembers) || previousMembers.length === 0) {
logger.debug('ClusterMembersComponent: First load or no previous members, performing full render');
this.render();
return;
}
if (this.shouldPreserveState(newMembers, previousMembers)) {
// Perform partial update to preserve UI state
logger.debug('ClusterMembersComponent: Preserving state, performing partial update');
this.updateMembersPartially(newMembers, previousMembers);
} else {
// Full re-render if structure changed significantly
logger.debug('ClusterMembersComponent: Structure changed, performing full re-render');
this.render();
}
}
// Handle loading state update
handleLoadingUpdate(isLoading) {
logger.debug('ClusterMembersComponent: Loading state changed:', isLoading);
if (isLoading) {
logger.debug('ClusterMembersComponent: Showing loading state');
this.renderLoading(`
Loading cluster members...
`);
// Set up a loading completion check
this.checkLoadingCompletion();
} else {
logger.debug('ClusterMembersComponent: Loading completed, checking if we need to render');
// When loading completes, check if we have data to render
this.handleLoadingCompletion();
}
}
// Check if loading has completed and handle accordingly
handleLoadingCompletion() {
const members = this.viewModel.get('members');
const error = this.viewModel.get('error');
const isLoading = this.viewModel.get('isLoading');
logger.debug('ClusterMembersComponent: Handling loading completion:', { members, error, isLoading });
if (error) {
logger.debug('ClusterMembersComponent: Loading completed with error, showing error state');
this.showErrorState(error);
} else if (members && members.length > 0) {
logger.debug('ClusterMembersComponent: Loading completed with data, rendering members');
this.renderMembers(members);
} else if (!isLoading) {
logger.debug('ClusterMembersComponent: Loading completed but no data, showing empty state');
this.showEmptyState();
}
}
// Set up a check to ensure loading completion is handled
checkLoadingCompletion() {
// Clear any existing completion check
if (this.loadingCompletionCheck) {
clearTimeout(this.loadingCompletionCheck);
}
// Set up a completion check that runs after a short delay
this.loadingCompletionCheck = setTimeout(() => {
const isLoading = this.viewModel.get('isLoading');
if (!isLoading) {
logger.debug('ClusterMembersComponent: Loading completion check triggered');
this.handleLoadingCompletion();
}
}, 1000); // Check after 1 second
}
// Handle error state update
handleErrorUpdate(error) {
if (error) {
this.showErrorState(error);
}
}
// Check if we should preserve UI state during update
shouldPreserveState(newMembers, previousMembers) {
if (!previousMembers || !Array.isArray(previousMembers)) return false;
if (!Array.isArray(newMembers)) return false;
// If member count changed, we need to re-render
if (newMembers.length !== previousMembers.length) return false;
// Check if member IPs are the same (same nodes)
const newIps = new Set(newMembers.map(m => m.ip));
const prevIps = new Set(previousMembers.map(m => m.ip));
// If IPs are the same, we can preserve state
return newIps.size === prevIps.size &&
[...newIps].every(ip => prevIps.has(ip));
}
// Check if we should skip rendering during view switches
shouldSkipRender() {
// Rely on lifecycle flags controlled by App
if (!this.isMounted || this.isPaused) {
logger.debug('ClusterMembersComponent: Not mounted or paused, skipping render');
return true;
}
return false;
}
// Update members partially to preserve UI state
updateMembersPartially(newMembers, previousMembers) {
logger.debug('ClusterMembersComponent: Performing partial update to preserve UI state');
// 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);
}
});
}
// Check if a specific member has changed
hasMemberChanged(newMember, prevMember) {
return newMember.status !== prevMember.status ||
newMember.latency !== prevMember.latency ||
newMember.hostname !== prevMember.hostname;
}
// Update a specific member card without re-rendering the entire component
updateMemberCard(member) {
const card = this.findElement(`[data-member-ip="${member.ip}"]`);
if (!card) return;
// Update status
const statusElement = card.querySelector('.member-status');
if (statusElement) {
const statusClass = member.status === 'active' ? 'status-online' : 'status-offline';
const statusIcon = member.status === 'active' ? '🟢' : '🔴';
statusElement.className = `member-status ${statusClass}`;
statusElement.innerHTML = `${statusIcon}`;
}
// Update latency
const latencyElement = card.querySelector('.latency-value');
if (latencyElement) {
latencyElement.textContent = member.latency ? member.latency + 'ms' : 'N/A';
}
// Update hostname if changed
const hostnameElement = card.querySelector('.member-hostname');
if (hostnameElement && member.hostname !== hostnameElement.textContent) {
hostnameElement.textContent = member.hostname || 'Unknown Device';
}
}
render() {
if (this.renderInProgress) {
logger.debug('ClusterMembersComponent: Render already in progress, skipping');
return;
}
// Check if we should skip rendering during view switches
if (this.shouldSkipRender()) {
return;
}
this.renderInProgress = true;
try {
logger.debug('ClusterMembersComponent: render() called');
logger.debug('ClusterMembersComponent: Container element:', this.container);
logger.debug('ClusterMembersComponent: Is mounted:', this.isMounted);
const members = this.viewModel.get('members');
const isLoading = this.viewModel.get('isLoading');
const error = this.viewModel.get('error');
logger.debug('ClusterMembersComponent: render data:', { members, isLoading, error });
if (isLoading) {
logger.debug('ClusterMembersComponent: Showing loading state');
this.showLoadingState();
return;
}
if (error) {
logger.debug('ClusterMembersComponent: Showing error state');
this.showErrorState(error);
return;
}
if (!members || members.length === 0) {
logger.debug('ClusterMembersComponent: Showing empty state');
this.showEmptyState();
return;
}
logger.debug('ClusterMembersComponent: Rendering members:', members);
this.renderMembers(members);
} finally {
this.renderInProgress = false;
}
}
// Show loading state
showLoadingState() {
logger.debug('ClusterMembersComponent: showLoadingState() called');
this.renderLoading(`
Loading cluster members...
`);
}
// Show error state
showErrorState(error) {
logger.debug('ClusterMembersComponent: showErrorState() called with error:', error);
this.renderError(`Error loading cluster members: ${error}`);
}
// Show empty state
showEmptyState() {
logger.debug('ClusterMembersComponent: showEmptyState() called');
this.renderEmpty(`