feat: improve styling

This commit is contained in:
2025-08-30 15:57:25 +02:00
parent 7bac42c58e
commit dc46fc6ca2
3 changed files with 204 additions and 57 deletions

View File

@@ -2905,8 +2905,10 @@ class MemberCardOverlayComponent extends Component {
setupEventListeners() { setupEventListeners() {
// Close overlay when clicking outside or pressing escape // Close overlay when clicking outside or pressing escape
this.addEventListener(document, 'click', (e) => { this.addEventListener(this.container, 'click', (e) => {
if (this.isVisible && !this.container.contains(e.target)) { if (!this.isVisible) return;
// Only close when clicking on the backdrop, not inside the dialog content
if (e.target === this.container) {
this.hide(); this.hide();
} }
}); });
@@ -2936,6 +2938,8 @@ class MemberCardOverlayComponent extends Component {
hide() { hide() {
this.isVisible = false; this.isVisible = false;
this.container.classList.remove('visible'); this.container.classList.remove('visible');
@@ -2977,11 +2981,9 @@ class MemberCardOverlayComponent extends Component {
<span class="latency-label">Latency:</span> <span class="latency-label">Latency:</span>
<span class="latency-value">${member.latency ? member.latency + 'ms' : 'N/A'}</span> <span class="latency-value">${member.latency ? member.latency + 'ms' : 'N/A'}</span>
</div> </div>
${member.labels && Object.keys(member.labels).length ? ` <div class="member-labels" style="display: none;">
<div class="member-labels"> <!-- Labels will be populated dynamically from node status API -->
${Object.entries(member.labels).map(([key, value]) => `<span class="label-chip">${key}: ${value}</span>`).join('')}
</div> </div>
` : ''}
</div> </div>
</div> </div>
<div class="member-details"> <div class="member-details">
@@ -3024,39 +3026,49 @@ class MemberCardOverlayComponent extends Component {
// Load node details // Load node details
await nodeDetailsVM.loadNodeDetails(memberIp); 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]) => `<span class="label-chip">${key}: ${value}</span>`)
.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]) => `<span class="label-chip">${key}: ${value}</span>`)
.join('');
// Insert after latency
const latencyDiv = memberInfo.querySelector('.member-latency');
if (latencyDiv) {
latencyDiv.parentNode.insertBefore(labelsDiv, latencyDiv.nextSibling);
}
}
}
}
// Mount the component // Mount the component
nodeDetailsComponent.mount(); nodeDetailsComponent.mount();
// Update UI // Update UI
card.classList.add('expanded'); card.classList.add('expanded');
const expandIcon = card.querySelector('.expand-icon');
if (expandIcon) {
expandIcon.classList.add('expanded');
}
} catch (error) { } catch (error) {
console.error('Failed to expand card:', error); console.error('Failed to expand member card:', error);
memberDetails.innerHTML = ` // Still show the UI even if details fail to load
<div class="error"> card.classList.add('expanded');
<strong>Error loading node details:</strong><br> const details = card.querySelector('.member-details');
${error.message} if (details) {
</div> details.innerHTML = '<div class="error">Failed to load node details</div>';
`;
} }
} }
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 = '<div class="loading-details">Loading detailed information...</div>';
} }
} }
}

View File

@@ -17,7 +17,13 @@ window.demoMembersData = {
api: [ api: [
{ uri: "/api/node/status", method: "GET" }, { uri: "/api/node/status", method: "GET" },
{ uri: "/api/tasks/status", method: "GET" } { uri: "/api/tasks/status", method: "GET" }
] ],
labels: {
environment: "production",
region: "us-west",
role: "worker",
cluster: "spore-main"
}
}, },
{ {
hostname: "spore-node-2", hostname: "spore-node-2",
@@ -35,7 +41,13 @@ window.demoMembersData = {
api: [ api: [
{ uri: "/api/node/status", method: "GET" }, { uri: "/api/node/status", method: "GET" },
{ uri: "/api/tasks/status", method: "GET" } { uri: "/api/tasks/status", method: "GET" }
] ],
labels: {
environment: "production",
region: "us-west",
role: "controller",
cluster: "spore-main"
}
}, },
{ {
hostname: "spore-node-3", hostname: "spore-node-3",
@@ -52,7 +64,13 @@ window.demoMembersData = {
}, },
api: [ api: [
{ uri: "/api/node/status", method: "GET" } { uri: "/api/node/status", method: "GET" }
] ],
labels: {
environment: "staging",
region: "us-west",
role: "worker",
cluster: "spore-main"
}
}, },
{ {
hostname: "spore-node-4", hostname: "spore-node-4",
@@ -71,7 +89,13 @@ window.demoMembersData = {
{ uri: "/api/node/status", method: "GET" }, { uri: "/api/node/status", method: "GET" },
{ uri: "/api/tasks/status", method: "GET" }, { uri: "/api/tasks/status", method: "GET" },
{ uri: "/api/capabilities", method: "GET" } { uri: "/api/capabilities", method: "GET" }
] ],
labels: {
environment: "production",
region: "us-east",
role: "gateway",
cluster: "spore-main"
}
}, },
{ {
hostname: "spore-node-5", hostname: "spore-node-5",
@@ -86,7 +110,8 @@ window.demoMembersData = {
cpuFreqMHz: 0, cpuFreqMHz: 0,
flashChipSize: 1048576 flashChipSize: 1048576
}, },
api: [] api: [],
} }
] ]
}; };
@@ -120,5 +145,30 @@ window.demoApiClient = {
]; ];
return { members: members.filter(m => m.ip !== ip) }; 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'
}
};
} }
}; };

View File

@@ -233,11 +233,11 @@ p {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
font-size: 0.75rem; font-size: 0.75rem;
padding: 0.15rem 0.45rem; padding: 0.25rem 0.6rem;
border-radius: 9999px; border-radius: 9999px;
background: rgba(255, 255, 255, 0.08); background: rgba(30, 58, 138, 0.35);
border: 1px solid rgba(255, 255, 255, 0.15); border: 1px solid rgba(59, 130, 246, 0.4);
color: rgba(255, 255, 255, 0.9); color: #dbeafe;
white-space: nowrap; white-space: nowrap;
} }
@@ -2164,9 +2164,9 @@ p {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 0.35rem; gap: 0.35rem;
padding-right: 0.25rem; padding-right: 0.35rem;
background: rgba(139, 92, 246, 0.15); background: rgba(30, 58, 138, 0.35);
border: 1px solid rgba(139, 92, 246, 0.35); border: 1px solid rgba(59, 130, 246, 0.55);
} }
.label-chip .chip-remove { .label-chip .chip-remove {
@@ -2491,12 +2491,12 @@ p {
} }
.member-overlay-body .member-card .member-header { .member-overlay-body .member-card .member-header {
padding: 20px 24px 16px; padding: 20px 24px 0px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1); border-bottom: none;
} }
.member-overlay-body .member-card .member-details { .member-overlay-body .member-card .member-details {
padding: 20px 24px; padding: 0px 24px;
} }
/* Hide expand icon in overlay since card is always expanded */ /* Hide expand icon in overlay since card is always expanded */
@@ -2509,6 +2509,91 @@ p {
display: block; 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 */ /* Highlight animation for member cards */
.member-card.highlighted { .member-card.highlighted {
animation: highlight-pulse 2s ease-in-out; animation: highlight-pulse 2s ease-in-out;