Compare commits
2 Commits
f28b4f8797
...
dc46fc6ca2
| Author | SHA1 | Date | |
|---|---|---|---|
| dc46fc6ca2 | |||
| 7bac42c58e |
@@ -2579,10 +2579,13 @@ class TopologyGraphComponent extends Component {
|
||||
this.simulation.alpha(0.3).restart();
|
||||
}
|
||||
|
||||
// Add click handlers for node selection
|
||||
// Add click handlers for node selection and member card overlay
|
||||
node.on('click', (event, d) => {
|
||||
this.viewModel.selectNode(d.id);
|
||||
this.updateSelection(d.id);
|
||||
|
||||
// Show member card overlay
|
||||
this.showMemberCardOverlay(d);
|
||||
});
|
||||
|
||||
// Add hover effects
|
||||
@@ -2820,6 +2823,52 @@ class TopologyGraphComponent extends Component {
|
||||
container.innerHTML = '<div class="no-data"><div>No cluster members found</div></div>';
|
||||
}
|
||||
|
||||
showMemberCardOverlay(nodeData) {
|
||||
// Create overlay container if it doesn't exist
|
||||
let overlayContainer = document.getElementById('member-card-overlay');
|
||||
if (!overlayContainer) {
|
||||
overlayContainer = document.createElement('div');
|
||||
overlayContainer.id = 'member-card-overlay';
|
||||
overlayContainer.className = 'member-card-overlay';
|
||||
document.body.appendChild(overlayContainer);
|
||||
}
|
||||
|
||||
// Create and show the overlay component
|
||||
if (!this.memberOverlayComponent) {
|
||||
const overlayVM = new ViewModel();
|
||||
this.memberOverlayComponent = new MemberCardOverlayComponent(overlayContainer, overlayVM, this.eventBus);
|
||||
this.memberOverlayComponent.mount();
|
||||
}
|
||||
|
||||
// Convert node data to member data format
|
||||
const memberData = {
|
||||
ip: nodeData.ip,
|
||||
hostname: nodeData.hostname,
|
||||
status: this.normalizeStatus(nodeData.status),
|
||||
latency: nodeData.latency,
|
||||
labels: nodeData.resources || {}
|
||||
};
|
||||
|
||||
this.memberOverlayComponent.show(memberData);
|
||||
}
|
||||
|
||||
// Normalize status from topology format to member card format
|
||||
normalizeStatus(status) {
|
||||
if (!status) return 'unknown';
|
||||
|
||||
const normalized = status.toLowerCase();
|
||||
switch (normalized) {
|
||||
case 'active':
|
||||
return 'active';
|
||||
case 'inactive':
|
||||
return 'inactive';
|
||||
case 'dead':
|
||||
return 'offline';
|
||||
default:
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
// Override render method to display the graph
|
||||
render() {
|
||||
console.log('TopologyGraphComponent: render called');
|
||||
@@ -2839,4 +2888,187 @@ class TopologyGraphComponent extends Component {
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// Member Card Overlay Component for displaying member details in topology view
|
||||
class MemberCardOverlayComponent extends Component {
|
||||
constructor(container, viewModel, eventBus) {
|
||||
super(container, viewModel, eventBus);
|
||||
this.isVisible = false;
|
||||
this.currentMember = null;
|
||||
}
|
||||
|
||||
mount() {
|
||||
super.mount();
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Close overlay when clicking outside or pressing escape
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
this.addEventListener(document, 'keydown', (e) => {
|
||||
if (e.key === 'Escape' && this.isVisible) {
|
||||
this.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
show(memberData) {
|
||||
this.currentMember = memberData;
|
||||
this.isVisible = true;
|
||||
|
||||
const memberCardHTML = this.renderMemberCard(memberData);
|
||||
this.setHTML('', memberCardHTML);
|
||||
|
||||
// Add visible class for animation
|
||||
setTimeout(() => {
|
||||
this.container.classList.add('visible');
|
||||
}, 10);
|
||||
|
||||
// Setup member card interactions
|
||||
this.setupMemberCardInteractions();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
hide() {
|
||||
this.isVisible = false;
|
||||
this.container.classList.remove('visible');
|
||||
this.currentMember = null;
|
||||
}
|
||||
|
||||
renderMemberCard(member) {
|
||||
const statusClass = member.status === 'active' ? 'status-online' :
|
||||
member.status === 'inactive' ? 'status-inactive' : 'status-offline';
|
||||
const statusText = member.status === 'active' ? 'Online' :
|
||||
member.status === 'inactive' ? 'Inactive' :
|
||||
member.status === 'offline' ? 'Offline' : 'Unknown';
|
||||
const statusIcon = member.status === 'active' ? '🟢' :
|
||||
member.status === 'inactive' ? '🟠' : '🔴';
|
||||
|
||||
return `
|
||||
<div class="member-overlay-content">
|
||||
<div class="member-overlay-header">
|
||||
<div class="member-overlay-title">
|
||||
<h3>Member Details</h3>
|
||||
</div>
|
||||
<button class="member-overlay-close" aria-label="Close">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M18 6L6 18M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="member-overlay-body">
|
||||
<div class="member-card expanded" data-member-ip="${member.ip}">
|
||||
<div class="member-header">
|
||||
<div class="member-info">
|
||||
<div class="member-name">${member.hostname || 'Unknown Device'}</div>
|
||||
<div class="member-ip">${member.ip || 'No IP'}</div>
|
||||
<div class="member-status ${statusClass}">
|
||||
${statusIcon} ${statusText}
|
||||
</div>
|
||||
<div class="member-latency">
|
||||
<span class="latency-label">Latency:</span>
|
||||
<span class="latency-value">${member.latency ? member.latency + 'ms' : 'N/A'}</span>
|
||||
</div>
|
||||
<div class="member-labels" style="display: none;">
|
||||
<!-- Labels will be populated dynamically from node status API -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="member-details">
|
||||
<div class="loading-details">Loading detailed information...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
setupMemberCardInteractions() {
|
||||
// Close button
|
||||
const closeBtn = this.findElement('.member-overlay-close');
|
||||
if (closeBtn) {
|
||||
this.addEventListener(closeBtn, 'click', () => {
|
||||
this.hide();
|
||||
});
|
||||
}
|
||||
|
||||
// Setup member card expansion - automatically expand when shown
|
||||
setTimeout(async () => {
|
||||
const memberCard = this.findElement('.member-card');
|
||||
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);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
async expandCard(card, memberIp, memberDetails) {
|
||||
try {
|
||||
// Create node details view model and component
|
||||
const nodeDetailsVM = new NodeDetailsViewModel();
|
||||
const nodeDetailsComponent = new NodeDetailsComponent(memberDetails, nodeDetailsVM, this.eventBus);
|
||||
|
||||
// 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]) => `<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
|
||||
nodeDetailsComponent.mount();
|
||||
|
||||
// Update UI
|
||||
card.classList.add('expanded');
|
||||
|
||||
} catch (error) {
|
||||
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 = '<div class="error">Failed to load node details</div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -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 {
|
||||
@@ -2383,6 +2383,286 @@ p {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Member Card Overlay Styles */
|
||||
.member-card-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.member-card-overlay.visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.member-overlay-content {
|
||||
background: linear-gradient(135deg, #1c2a38 0%, #283746 50%, #1a252f 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5);
|
||||
max-width: 800px;
|
||||
width: 90%;
|
||||
max-height: 90vh;
|
||||
overflow: hidden;
|
||||
transform: scale(0.9) translateY(20px);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.member-card-overlay.visible .member-overlay-content {
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
|
||||
.member-overlay-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
padding: 24px 24px 16px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.member-overlay-title h3 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #ecf0f1;
|
||||
}
|
||||
|
||||
.member-overlay-subtitle {
|
||||
font-size: 1rem;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.member-overlay-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.member-overlay-close:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.member-overlay-close svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.member-overlay-body {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.member-overlay-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
/* Member card container within overlay */
|
||||
.member-overlay-body {
|
||||
padding: 0;
|
||||
overflow: auto;
|
||||
max-height: calc(90vh - 120px); /* Account for header */
|
||||
}
|
||||
|
||||
/* Ensure member cards render properly in overlay */
|
||||
.member-overlay-body .member-card {
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.member-overlay-body .member-card .member-header {
|
||||
padding: 20px 24px 0px;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.member-overlay-body .member-card .member-details {
|
||||
padding: 0px 24px;
|
||||
}
|
||||
|
||||
/* Hide expand icon in overlay since card is always expanded */
|
||||
.member-overlay-body .member-card .expand-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Ensure expanded state is visually clear */
|
||||
.member-overlay-body .member-card.expanded .member-details {
|
||||
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;
|
||||
}
|
||||
|
||||
@keyframes highlight-pulse {
|
||||
0%, 100% {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 8px 32px rgba(59, 130, 246, 0.4), 0 4px 16px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive design for overlay */
|
||||
@media (max-width: 768px) {
|
||||
.member-overlay-content {
|
||||
width: 95%;
|
||||
max-width: none;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.member-overlay-header {
|
||||
padding: 20px 20px 12px;
|
||||
}
|
||||
|
||||
.member-overlay-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.member-overlay-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.member-overlay-title h3 {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Test page styles */
|
||||
.test-section {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.test-section h2 {
|
||||
color: #ecf0f1;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
#test-overlay-btn {
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
#test-overlay-btn:hover {
|
||||
background: linear-gradient(135deg, #2563eb 0%, #1e40af 100%);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Loading and error states */
|
||||
.loading, .error, .no-data {
|
||||
display: flex;
|
||||
|
||||
58
public/test-member-overlay.html
Normal file
58
public/test-member-overlay.html
Normal file
@@ -0,0 +1,58 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Test Member Card Overlay</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<script src="./d3.v7.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Test Member Card Overlay</h1>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>Test Data</h2>
|
||||
<button id="test-overlay-btn">Show Test Member Overlay</button>
|
||||
</div>
|
||||
|
||||
<div id="member-card-overlay" class="member-card-overlay"></div>
|
||||
</div>
|
||||
|
||||
<script src="./framework.js"></script>
|
||||
<script src="./components.js"></script>
|
||||
<script>
|
||||
// Test the member card overlay
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const testBtn = document.getElementById('test-overlay-btn');
|
||||
const overlayContainer = document.getElementById('member-card-overlay');
|
||||
|
||||
// Create test view model and event bus
|
||||
const testVM = new ViewModel();
|
||||
const testEventBus = new EventBus();
|
||||
|
||||
// Create the overlay component
|
||||
const overlayComponent = new MemberCardOverlayComponent(overlayContainer, testVM, testEventBus);
|
||||
overlayComponent.mount();
|
||||
|
||||
// Test data
|
||||
const testMember = {
|
||||
ip: '192.168.1.100',
|
||||
hostname: 'test-node-01',
|
||||
status: 'active',
|
||||
latency: 15,
|
||||
labels: {
|
||||
'environment': 'production',
|
||||
'region': 'us-west',
|
||||
'role': 'worker'
|
||||
}
|
||||
};
|
||||
|
||||
// Show overlay on button click
|
||||
testBtn.addEventListener('click', function() {
|
||||
overlayComponent.show(testMember);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
63
public/test-topology-overlay.html
Normal file
63
public/test-topology-overlay.html
Normal file
@@ -0,0 +1,63 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Test Topology with Member Card Overlay</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<script src="./d3.v7.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Test Topology with Member Card Overlay</h1>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>Topology View with Clickable Nodes</h2>
|
||||
<p>Click on any node in the topology to see the member card overlay.</p>
|
||||
<button id="refresh-topology-btn">Refresh Topology</button>
|
||||
</div>
|
||||
|
||||
<div id="topology-graph-container">
|
||||
<div class="loading">
|
||||
<div>Loading network topology...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="member-card-overlay" class="member-card-overlay"></div>
|
||||
</div>
|
||||
|
||||
<script src="./demo-topology-data.js"></script>
|
||||
<script src="./framework.js"></script>
|
||||
<script src="./components.js"></script>
|
||||
<script>
|
||||
// Test the topology view with member card overlay
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('Setting up test topology with member card overlay...');
|
||||
|
||||
// Override the API client with demo data
|
||||
window.apiClient = window.demoApiClient;
|
||||
|
||||
// Create topology view model
|
||||
const topologyVM = new TopologyViewModel();
|
||||
|
||||
// Create the topology component
|
||||
const topologyComponent = new TopologyGraphComponent(
|
||||
document.getElementById('topology-graph-container'),
|
||||
topologyVM,
|
||||
new EventBus()
|
||||
);
|
||||
|
||||
// Mount the component
|
||||
topologyComponent.mount();
|
||||
|
||||
// Refresh button
|
||||
const refreshBtn = document.getElementById('refresh-topology-btn');
|
||||
refreshBtn.addEventListener('click', function() {
|
||||
topologyVM.updateNetworkTopology();
|
||||
});
|
||||
|
||||
console.log('Test topology setup complete');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user