From 13f771837b9759648a09bc42fc203a1b2f8d6bbd Mon Sep 17 00:00:00 2001 From: Patrick Balsiger Date: Tue, 16 Sep 2025 14:44:55 +0200 Subject: [PATCH] feat: highlight selected node --- .../components/ClusterMembersComponent.js | 35 +++++++++++++++++++ public/scripts/components/DrawerComponent.js | 10 +++++- public/styles/main.css | 31 ++++++++++++++++ public/styles/theme.css | 27 ++++++++++++++ 4 files changed, 102 insertions(+), 1 deletion(-) diff --git a/public/scripts/components/ClusterMembersComponent.js b/public/scripts/components/ClusterMembersComponent.js index 7b89fb4..e5fbb8f 100644 --- a/public/scripts/components/ClusterMembersComponent.js +++ b/public/scripts/components/ClusterMembersComponent.js @@ -22,6 +22,9 @@ class ClusterMembersComponent extends Component { // Drawer state for desktop this.drawer = new DrawerComponent(); + + // Selection state for highlighting + this.selectedMemberIp = null; } // Determine if we should use desktop drawer behavior @@ -31,6 +34,9 @@ class ClusterMembersComponent extends Component { openDrawerForMember(memberIp) { + // Set selected member and update highlighting + this.setSelectedMember(memberIp); + // Get display name for drawer title let displayName = memberIp; try { @@ -68,6 +74,9 @@ class ClusterMembersComponent extends Component { `; }); + }, null, () => { + // Close callback - clear selection when drawer is closed + this.clearSelectedMember(); }); } @@ -687,6 +696,32 @@ class ClusterMembersComponent extends Component { // Don't re-render on resume - maintain current state return false; } + + // Set selected member and update highlighting + setSelectedMember(memberIp) { + // Clear previous selection + this.clearSelectedMember(); + + // Set new selection + this.selectedMemberIp = memberIp; + + // Add selected class to the member card + const card = this.findElement(`[data-member-ip="${memberIp}"]`); + if (card) { + card.classList.add('selected'); + } + } + + // Clear selected member highlighting + clearSelectedMember() { + if (this.selectedMemberIp) { + const card = this.findElement(`[data-member-ip="${this.selectedMemberIp}"]`); + if (card) { + card.classList.remove('selected'); + } + this.selectedMemberIp = null; + } + } } window.ClusterMembersComponent = ClusterMembersComponent; \ No newline at end of file diff --git a/public/scripts/components/DrawerComponent.js b/public/scripts/components/DrawerComponent.js index 3f8930f..56b80e3 100644 --- a/public/scripts/components/DrawerComponent.js +++ b/public/scripts/components/DrawerComponent.js @@ -5,6 +5,7 @@ class DrawerComponent { this.detailsDrawerContent = null; this.detailsDrawerBackdrop = null; this.activeDrawerComponent = null; + this.onCloseCallback = null; } // Determine if we should use desktop drawer behavior @@ -57,8 +58,9 @@ class DrawerComponent { }); } - openDrawer(title, contentCallback, errorCallback) { + openDrawer(title, contentCallback, errorCallback, onCloseCallback) { this.ensureDrawer(); + this.onCloseCallback = onCloseCallback; // Set drawer title const titleEl = this.detailsDrawer.querySelector('.drawer-title'); @@ -101,6 +103,12 @@ class DrawerComponent { closeDrawer() { if (this.detailsDrawer) this.detailsDrawer.classList.remove('open'); if (this.detailsDrawerBackdrop) this.detailsDrawerBackdrop.classList.remove('visible'); + + // Call close callback if provided + if (this.onCloseCallback) { + this.onCloseCallback(); + this.onCloseCallback = null; + } } // Clean up drawer elements diff --git a/public/styles/main.css b/public/styles/main.css index fbf0b42..b393d54 100644 --- a/public/styles/main.css +++ b/public/styles/main.css @@ -3033,6 +3033,37 @@ p { animation: highlight-pulse 2s ease-in-out; } +/* Selected member card styling */ +.member-card.selected { + border-color: #3b82f6 !important; + border-width: 2px !important; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2), 0 8px 25px rgba(0, 0, 0, 0.2) !important; + background: linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, var(--bg-tertiary) 100%) !important; + z-index: 10 !important; +} + +.member-card.selected::before { + opacity: 0.8 !important; + background: linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, rgba(59, 130, 246, 0.05) 100%) !important; +} + +.member-card.selected .member-header { + background: rgba(59, 130, 246, 0.05) !important; +} + +.member-card.selected .member-status { + filter: brightness(1.2) !important; +} + +.member-card.selected .member-hostname { + color: #3b82f6 !important; + font-weight: 600 !important; +} + +.member-card.selected .member-ip { + color: #60a5fa !important; +} + @keyframes highlight-pulse { 0%, 100% { box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); diff --git a/public/styles/theme.css b/public/styles/theme.css index 084e05e..6e7bd76 100644 --- a/public/styles/theme.css +++ b/public/styles/theme.css @@ -236,6 +236,33 @@ display: none; } +/* Selected member card styling for light theme */ +[data-theme="light"] .member-card.selected { + border-color: #3b82f6 !important; + border-width: 2px !important; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2), 0 8px 25px rgba(148, 163, 184, 0.15) !important; + background: linear-gradient(135deg, rgba(59, 130, 246, 0.08) 0%, rgba(255, 255, 255, 0.15) 100%) !important; +} + +[data-theme="light"] .member-card.selected::before { + display: block !important; + opacity: 0.6 !important; + background: linear-gradient(135deg, rgba(59, 130, 246, 0.08) 0%, rgba(59, 130, 246, 0.03) 100%) !important; +} + +[data-theme="light"] .member-card.selected .member-header { + background: rgba(59, 130, 246, 0.03) !important; +} + +[data-theme="light"] .member-card.selected .member-hostname { + color: #1d4ed8 !important; + font-weight: 600 !important; +} + +[data-theme="light"] .member-card.selected .member-ip { + color: #3b82f6 !important; +} + [data-theme="light"] .tabs-container { background: rgba(255, 255, 255, 0.08); backdrop-filter: blur(20px);