diff --git a/public/components.js b/public/components.js
index 50ee30a..e84930a 100644
--- a/public/components.js
+++ b/public/components.js
@@ -2905,8 +2905,10 @@ class MemberCardOverlayComponent extends Component {
setupEventListeners() {
// Close overlay when clicking outside or pressing escape
- this.addEventListener(document, 'click', (e) => {
- if (this.isVisible && !this.container.contains(e.target)) {
+ this.addEventListener(this.container, 'click', (e) => {
+ if (!this.isVisible) return;
+ // Only close when clicking on the backdrop, not inside the dialog content
+ if (e.target === this.container) {
this.hide();
}
});
@@ -2934,8 +2936,10 @@ class MemberCardOverlayComponent extends Component {
this.setupMemberCardInteractions();
}
-
-
+
+
+
+
hide() {
this.isVisible = false;
this.container.classList.remove('visible');
@@ -2977,11 +2981,9 @@ class MemberCardOverlayComponent extends Component {
Latency:
${member.latency ? member.latency + 'ms' : 'N/A'}
- ${member.labels && Object.keys(member.labels).length ? `
-
@@ -3008,7 +3010,7 @@ class MemberCardOverlayComponent extends Component {
if (memberCard) {
const memberDetails = memberCard.querySelector('.member-details');
const memberIp = memberCard.dataset.memberIp;
-
+
// Automatically expand the card to show details
await this.expandCard(memberCard, memberIp, memberDetails);
}
@@ -3024,39 +3026,49 @@ class MemberCardOverlayComponent extends Component {
// Load node details
await nodeDetailsVM.loadNodeDetails(memberIp);
+ // Update the labels in the member header with the actual node status data
+ const nodeStatus = nodeDetailsVM.get('nodeStatus');
+ if (nodeStatus && nodeStatus.labels) {
+ const labelsContainer = card.querySelector('.member-labels');
+ if (labelsContainer) {
+ // Update existing labels container and show it
+ labelsContainer.innerHTML = Object.entries(nodeStatus.labels)
+ .map(([key, value]) => `
${key}: ${value}`)
+ .join('');
+ labelsContainer.style.display = 'block';
+ } else {
+ // Create new labels container if it doesn't exist
+ const memberInfo = card.querySelector('.member-info');
+ if (memberInfo) {
+ const labelsDiv = document.createElement('div');
+ labelsDiv.className = 'member-labels';
+ labelsDiv.innerHTML = Object.entries(nodeStatus.labels)
+ .map(([key, value]) => `
${key}: ${value}`)
+ .join('');
+
+ // Insert after latency
+ const latencyDiv = memberInfo.querySelector('.member-latency');
+ if (latencyDiv) {
+ latencyDiv.parentNode.insertBefore(labelsDiv, latencyDiv.nextSibling);
+ }
+ }
+ }
+ }
+
// Mount the component
nodeDetailsComponent.mount();
// Update UI
card.classList.add('expanded');
- const expandIcon = card.querySelector('.expand-icon');
- if (expandIcon) {
- expandIcon.classList.add('expanded');
- }
} catch (error) {
- console.error('Failed to expand card:', error);
- memberDetails.innerHTML = `
-
- Error loading node details:
- ${error.message}
-
- `;
+ console.error('Failed to expand member card:', error);
+ // Still show the UI even if details fail to load
+ card.classList.add('expanded');
+ const details = card.querySelector('.member-details');
+ if (details) {
+ details.innerHTML = '
Failed to load node details
';
+ }
}
}
-
- collapseCard(card, expandIcon) {
- card.classList.remove('expanded');
- if (expandIcon) {
- expandIcon.classList.remove('expanded');
- }
-
- // Reset member details to loading state
- const memberDetails = card.querySelector('.member-details');
- if (memberDetails) {
- memberDetails.innerHTML = '
Loading detailed information...
';
- }
- }
-
-
}
\ No newline at end of file
diff --git a/public/demo-topology-data.js b/public/demo-topology-data.js
index a30332d..917e76f 100644
--- a/public/demo-topology-data.js
+++ b/public/demo-topology-data.js
@@ -17,7 +17,13 @@ window.demoMembersData = {
api: [
{ uri: "/api/node/status", method: "GET" },
{ uri: "/api/tasks/status", method: "GET" }
- ]
+ ],
+ labels: {
+ environment: "production",
+ region: "us-west",
+ role: "worker",
+ cluster: "spore-main"
+ }
},
{
hostname: "spore-node-2",
@@ -35,7 +41,13 @@ window.demoMembersData = {
api: [
{ uri: "/api/node/status", method: "GET" },
{ uri: "/api/tasks/status", method: "GET" }
- ]
+ ],
+ labels: {
+ environment: "production",
+ region: "us-west",
+ role: "controller",
+ cluster: "spore-main"
+ }
},
{
hostname: "spore-node-3",
@@ -52,7 +64,13 @@ window.demoMembersData = {
},
api: [
{ uri: "/api/node/status", method: "GET" }
- ]
+ ],
+ labels: {
+ environment: "staging",
+ region: "us-west",
+ role: "worker",
+ cluster: "spore-main"
+ }
},
{
hostname: "spore-node-4",
@@ -71,7 +89,13 @@ window.demoMembersData = {
{ uri: "/api/node/status", method: "GET" },
{ uri: "/api/tasks/status", method: "GET" },
{ uri: "/api/capabilities", method: "GET" }
- ]
+ ],
+ labels: {
+ environment: "production",
+ region: "us-east",
+ role: "gateway",
+ cluster: "spore-main"
+ }
},
{
hostname: "spore-node-5",
@@ -86,7 +110,8 @@ window.demoMembersData = {
cpuFreqMHz: 0,
flashChipSize: 1048576
},
- api: []
+ api: [],
+
}
]
};
@@ -120,5 +145,30 @@ window.demoApiClient = {
];
return { members: members.filter(m => m.ip !== ip) };
+ },
+
+ async getNodeStatus(ip) {
+ // Simulate network delay
+ await new Promise(resolve => setTimeout(resolve, 300));
+
+ // Find the member by IP
+ const member = window.demoMembersData.members.find(m => m.ip === ip);
+ if (!member) {
+ throw new Error('Node not found');
+ }
+
+ // Return node status with labels
+ return {
+ ...member.resources,
+ api: member.api,
+ labels: {
+ environment: ip.includes('103') ? 'production' : 'production',
+ region: ip.includes('103') ? 'us-east' : 'us-west',
+ role: ip.includes('101') ? 'controller' : ip.includes('103') ? 'gateway' : 'worker',
+ cluster: 'spore-main',
+ nodeType: ip.includes('102') ? 'staging' : 'production',
+ location: ip.includes('103') ? 'datacenter-2' : 'datacenter-1'
+ }
+ };
}
};
\ No newline at end of file
diff --git a/public/styles.css b/public/styles.css
index 5310f13..d7cf09d 100644
--- a/public/styles.css
+++ b/public/styles.css
@@ -229,17 +229,17 @@ p {
margin-top: 0.5rem;
}
-.label-chip {
- display: inline-flex;
- align-items: center;
- font-size: 0.75rem;
- padding: 0.15rem 0.45rem;
- border-radius: 9999px;
- background: rgba(255, 255, 255, 0.08);
- border: 1px solid rgba(255, 255, 255, 0.15);
- color: rgba(255, 255, 255, 0.9);
- white-space: nowrap;
-}
+ .label-chip {
+ display: inline-flex;
+ align-items: center;
+ font-size: 0.75rem;
+ padding: 0.25rem 0.6rem;
+ border-radius: 9999px;
+ background: rgba(30, 58, 138, 0.35);
+ border: 1px solid rgba(59, 130, 246, 0.4);
+ color: #dbeafe;
+ white-space: nowrap;
+ }
.member-card::before {
content: '';
@@ -2164,9 +2164,9 @@ p {
display: inline-flex;
align-items: center;
gap: 0.35rem;
- padding-right: 0.25rem;
- background: rgba(139, 92, 246, 0.15);
- border: 1px solid rgba(139, 92, 246, 0.35);
+ padding-right: 0.35rem;
+ background: rgba(30, 58, 138, 0.35);
+ border: 1px solid rgba(59, 130, 246, 0.55);
}
.label-chip .chip-remove {
@@ -2491,12 +2491,12 @@ p {
}
.member-overlay-body .member-card .member-header {
- padding: 20px 24px 16px;
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+ padding: 20px 24px 0px;
+ border-bottom: none;
}
.member-overlay-body .member-card .member-details {
- padding: 20px 24px;
+ padding: 0px 24px;
}
/* Hide expand icon in overlay since card is always expanded */
@@ -2509,6 +2509,91 @@ p {
display: block;
}
+/* Disable hover effects on topology dialog member cards */
+.member-overlay-body .member-card:hover {
+ transform: none !important;
+ box-shadow: none !important;
+}
+
+.member-overlay-body .member-card::before {
+ display: none !important;
+}
+
+.member-overlay-body .member-card .member-header:hover {
+ background: none !important;
+}
+
+/* Label chips styling for overlay */
+.member-overlay-body .member-labels {
+ margin-top: 16px;
+}
+
+/* unified with .label-chip */
+.member-overlay-body .label-chip {
+ margin-right: 10px;
+ background: rgba(30, 58, 138, 0.35);
+ color: #dbeafe;
+ padding: 0.25rem 0.6rem;
+ border-radius: 9999px;
+ font-size: 0.75rem;
+ border: 1px solid rgba(59, 130, 246, 0.4);
+ font-family: inherit;
+ white-space: nowrap;
+ display: inline-flex;
+}
+
+/* Labels section styling in node details */
+.detail-section {
+ margin-top: 20px;
+ padding-top: 20px;
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+.detail-section-title {
+ color: rgba(255, 255, 255, 0.8);
+ font-size: 1rem;
+ font-weight: 600;
+ margin-bottom: 16px;
+ text-transform: capitalize;
+}
+
+/* Resource and API chips styling */
+.member-resources, .member-api {
+ margin-top: 16px;
+}
+
+.resources-title, .api-title {
+ color: rgba(255, 255, 255, 0.7);
+ font-size: 0.9rem;
+ margin-bottom: 12px;
+ font-weight: 500;
+}
+
+.resources-container, .api-container {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.resource-chip, .api-chip {
+ background: rgba(59, 130, 246, 0.2);
+ color: #60a5fa;
+ padding: 4px 12px;
+ border-radius: 16px;
+ font-size: 0.8rem;
+ border: 1px solid rgba(59, 130, 246, 0.3);
+ font-family: 'Courier New', monospace;
+ white-space: nowrap;
+}
+
+
+
+.api-chip {
+ background: rgba(16, 185, 129, 0.2);
+ color: #10b981;
+ border-color: rgba(16, 185, 129, 0.3);
+}
+
/* Highlight animation for member cards */
.member-card.highlighted {
animation: highlight-pulse 2s ease-in-out;