feat: memberlist filter
This commit is contained in:
@@ -28,6 +28,13 @@ class ClusterMembersComponent extends Component {
|
||||
|
||||
// Selection state for highlighting
|
||||
this.selectedMemberIp = null;
|
||||
|
||||
// Filter state
|
||||
this.currentFilter = {
|
||||
labelKey: '',
|
||||
labelValue: ''
|
||||
};
|
||||
this.allMembers = []; // Store unfiltered members
|
||||
}
|
||||
|
||||
// Determine if we should use desktop drawer behavior
|
||||
@@ -35,6 +42,219 @@ class ClusterMembersComponent extends Component {
|
||||
return this.drawer.isDesktop();
|
||||
}
|
||||
|
||||
// Extract all unique label keys from members
|
||||
extractLabelKeys(members) {
|
||||
const labelKeys = new Set();
|
||||
|
||||
members.forEach(member => {
|
||||
if (member.labels && typeof member.labels === 'object') {
|
||||
Object.keys(member.labels).forEach(key => {
|
||||
labelKeys.add(key);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(labelKeys).sort();
|
||||
}
|
||||
|
||||
// Extract unique values for a specific label key from members
|
||||
extractLabelValuesForKey(members, labelKey) {
|
||||
const labelValues = new Set();
|
||||
|
||||
members.forEach(member => {
|
||||
if (member.labels && typeof member.labels === 'object' && member.labels[labelKey]) {
|
||||
labelValues.add(member.labels[labelKey]);
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(labelValues).sort();
|
||||
}
|
||||
|
||||
// Filter members based on current filter criteria
|
||||
filterMembers(members) {
|
||||
if (!this.currentFilter.labelKey && !this.currentFilter.labelValue) {
|
||||
return members;
|
||||
}
|
||||
|
||||
return members.filter(member => {
|
||||
if (!member.labels || typeof member.labels !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If only label key is specified, show all members with that key
|
||||
if (this.currentFilter.labelKey && !this.currentFilter.labelValue) {
|
||||
return member.labels.hasOwnProperty(this.currentFilter.labelKey);
|
||||
}
|
||||
|
||||
// If both key and value are specified, show members with exact match
|
||||
if (this.currentFilter.labelKey && this.currentFilter.labelValue) {
|
||||
return member.labels[this.currentFilter.labelKey] === this.currentFilter.labelValue;
|
||||
}
|
||||
|
||||
// If only value is specified, show members with that value for any key
|
||||
if (!this.currentFilter.labelKey && this.currentFilter.labelValue) {
|
||||
return Object.values(member.labels).includes(this.currentFilter.labelValue);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
// Update filter dropdowns with current label data
|
||||
updateFilterDropdowns() {
|
||||
// Always use unfiltered member list for dropdown options
|
||||
const labelKeys = this.extractLabelKeys(this.allMembers);
|
||||
|
||||
// Update label key dropdown
|
||||
const keySelect = document.getElementById('label-key-filter');
|
||||
if (keySelect) {
|
||||
const currentKey = keySelect.value;
|
||||
keySelect.innerHTML = '<option value="">All Labels</option>';
|
||||
labelKeys.forEach(key => {
|
||||
const option = document.createElement('option');
|
||||
option.value = key;
|
||||
option.textContent = key;
|
||||
keySelect.appendChild(option);
|
||||
});
|
||||
|
||||
// Restore selection if it still exists
|
||||
if (currentKey && labelKeys.includes(currentKey)) {
|
||||
keySelect.value = currentKey;
|
||||
} else {
|
||||
keySelect.value = '';
|
||||
this.currentFilter.labelKey = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Update label value dropdown based on selected key
|
||||
this.updateValueDropdown();
|
||||
|
||||
// Update clear button state
|
||||
this.updateClearButtonState();
|
||||
}
|
||||
|
||||
// Update clear button state based on current filter
|
||||
updateClearButtonState() {
|
||||
const clearBtn = document.getElementById('clear-filters-btn');
|
||||
if (clearBtn) {
|
||||
const hasFilters = this.currentFilter.labelKey || this.currentFilter.labelValue;
|
||||
clearBtn.disabled = !hasFilters;
|
||||
logger.debug('ClusterMembersComponent: Clear button state updated:', {
|
||||
hasFilters,
|
||||
disabled: clearBtn.disabled,
|
||||
currentFilter: this.currentFilter
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update the value dropdown based on the currently selected key
|
||||
updateValueDropdown() {
|
||||
const valueSelect = document.getElementById('label-value-filter');
|
||||
if (!valueSelect) return;
|
||||
|
||||
const currentValue = valueSelect.value;
|
||||
valueSelect.innerHTML = '<option value="">All Values</option>';
|
||||
|
||||
// If a key is selected, show only values for that key
|
||||
if (this.currentFilter.labelKey) {
|
||||
const labelValues = this.extractLabelValuesForKey(this.allMembers, this.currentFilter.labelKey);
|
||||
labelValues.forEach(value => {
|
||||
const option = document.createElement('option');
|
||||
option.value = value;
|
||||
option.textContent = value;
|
||||
valueSelect.appendChild(option);
|
||||
});
|
||||
|
||||
// Restore selection if it still exists for this key
|
||||
if (currentValue && labelValues.includes(currentValue)) {
|
||||
valueSelect.value = currentValue;
|
||||
} else {
|
||||
valueSelect.value = '';
|
||||
this.currentFilter.labelValue = '';
|
||||
}
|
||||
} else {
|
||||
// If no key is selected, show all unique values from all keys
|
||||
const allValues = new Set();
|
||||
this.allMembers.forEach(member => {
|
||||
if (member.labels && typeof member.labels === 'object') {
|
||||
Object.values(member.labels).forEach(value => {
|
||||
allValues.add(value);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const sortedValues = Array.from(allValues).sort();
|
||||
sortedValues.forEach(value => {
|
||||
const option = document.createElement('option');
|
||||
option.value = value;
|
||||
option.textContent = value;
|
||||
valueSelect.appendChild(option);
|
||||
});
|
||||
|
||||
// Restore selection if it still exists
|
||||
if (currentValue && sortedValues.includes(currentValue)) {
|
||||
valueSelect.value = currentValue;
|
||||
} else {
|
||||
valueSelect.value = '';
|
||||
this.currentFilter.labelValue = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply filter and re-render
|
||||
applyFilter() {
|
||||
const filteredMembers = this.filterMembers(this.allMembers);
|
||||
this.renderFilteredMembers(filteredMembers);
|
||||
}
|
||||
|
||||
// Set up filter event listeners
|
||||
setupFilterListeners() {
|
||||
logger.debug('ClusterMembersComponent: Setting up filter listeners...');
|
||||
const keySelect = document.getElementById('label-key-filter');
|
||||
const valueSelect = document.getElementById('label-value-filter');
|
||||
const clearBtn = document.getElementById('clear-filters-btn');
|
||||
|
||||
logger.debug('ClusterMembersComponent: Filter elements found:', {
|
||||
keySelect: !!keySelect,
|
||||
valueSelect: !!valueSelect,
|
||||
clearBtn: !!clearBtn
|
||||
});
|
||||
|
||||
if (keySelect) {
|
||||
keySelect.addEventListener('change', (e) => {
|
||||
this.currentFilter.labelKey = e.target.value;
|
||||
// When key changes, reset value and update value dropdown
|
||||
this.currentFilter.labelValue = '';
|
||||
this.updateValueDropdown();
|
||||
this.updateClearButtonState();
|
||||
this.applyFilter();
|
||||
});
|
||||
}
|
||||
|
||||
if (valueSelect) {
|
||||
valueSelect.addEventListener('change', (e) => {
|
||||
this.currentFilter.labelValue = e.target.value;
|
||||
this.updateClearButtonState();
|
||||
this.applyFilter();
|
||||
});
|
||||
}
|
||||
|
||||
if (clearBtn) {
|
||||
clearBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
logger.debug('ClusterMembersComponent: Clear filters button clicked');
|
||||
this.currentFilter = { labelKey: '', labelValue: '' };
|
||||
if (keySelect) keySelect.value = '';
|
||||
if (valueSelect) valueSelect.value = '';
|
||||
this.updateClearButtonState();
|
||||
this.applyFilter();
|
||||
});
|
||||
} else {
|
||||
logger.warn('ClusterMembersComponent: Clear filters button not found');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
openDrawerForMember(memberIp) {
|
||||
// Set selected member and update highlighting
|
||||
@@ -98,6 +318,9 @@ class ClusterMembersComponent extends Component {
|
||||
// Set up loading timeout safeguard
|
||||
this.setupLoadingTimeout();
|
||||
|
||||
// Set up filter listeners
|
||||
this.setupFilterListeners();
|
||||
|
||||
logger.debug('ClusterMembersComponent: Mounted successfully');
|
||||
}
|
||||
|
||||
@@ -413,7 +636,16 @@ class ClusterMembersComponent extends Component {
|
||||
renderMembers(members) {
|
||||
logger.debug('ClusterMembersComponent: renderMembers() called with', members.length, 'members');
|
||||
|
||||
const membersHTML = members.map(member => {
|
||||
// Store all members for filtering
|
||||
this.allMembers = members;
|
||||
|
||||
// Update filter dropdowns with current label data
|
||||
this.updateFilterDropdowns();
|
||||
|
||||
// Apply current filter to get filtered members
|
||||
const filteredMembers = this.filterMembers(members);
|
||||
|
||||
const membersHTML = filteredMembers.map(member => {
|
||||
let statusClass, statusIcon;
|
||||
if (member.status && member.status.toUpperCase() === 'ACTIVE') {
|
||||
statusClass = 'status-online';
|
||||
@@ -477,7 +709,84 @@ class ClusterMembersComponent extends Component {
|
||||
logger.debug('ClusterMembersComponent: Setting HTML, length:', membersHTML.length);
|
||||
this.setHTML('', membersHTML);
|
||||
logger.debug('ClusterMembersComponent: HTML set, setting up member cards...');
|
||||
this.setupMemberCards(members);
|
||||
this.setupMemberCards(filteredMembers);
|
||||
}
|
||||
|
||||
// Apply filter and re-render
|
||||
applyFilter() {
|
||||
const filteredMembers = this.filterMembers(this.allMembers);
|
||||
this.renderFilteredMembers(filteredMembers);
|
||||
}
|
||||
|
||||
// Render filtered members without updating dropdowns
|
||||
renderFilteredMembers(filteredMembers) {
|
||||
logger.debug('ClusterMembersComponent: renderFilteredMembers() called with', filteredMembers.length, 'members');
|
||||
|
||||
const membersHTML = filteredMembers.map(member => {
|
||||
let statusClass, statusIcon;
|
||||
if (member.status && member.status.toUpperCase() === 'ACTIVE') {
|
||||
statusClass = 'status-online';
|
||||
statusIcon = window.icon('dotGreen', { width: 12, height: 12 });
|
||||
} else if (member.status && member.status.toUpperCase() === 'INACTIVE') {
|
||||
statusClass = 'status-dead';
|
||||
statusIcon = window.icon('dotRed', { width: 12, height: 12 });
|
||||
} else {
|
||||
statusClass = 'status-offline';
|
||||
statusIcon = window.icon('dotRed', { width: 12, height: 12 });
|
||||
}
|
||||
|
||||
logger.debug('ClusterMembersComponent: Rendering member:', member);
|
||||
|
||||
return `
|
||||
<div class="member-card" data-member-ip="${member.ip}">
|
||||
<div class="member-header">
|
||||
<div class="member-info">
|
||||
<div class="member-row-1">
|
||||
<div class="status-hostname-group">
|
||||
<div class="member-status ${statusClass}">
|
||||
${statusIcon}
|
||||
</div>
|
||||
<div class="member-hostname">${this.escapeHtml(member.hostname || 'Unknown Device')}</div>
|
||||
</div>
|
||||
<div class="member-ip">${this.escapeHtml(member.ip || 'No IP')}</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>
|
||||
${member.labels && Object.keys(member.labels).length ? `
|
||||
<div class="member-row-2">
|
||||
<div class="member-labels">
|
||||
${Object.entries(member.labels).map(([key, value]) => `<span class=\"label-chip\">${this.escapeHtml(key)}: ${this.escapeHtml(value)}</span>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
<div class="member-actions">
|
||||
<button class="member-terminal-btn" title="Open Terminal" aria-label="Open Terminal">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M4 17l6-6-6-6"></path>
|
||||
<path d="M12 19h8"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="expand-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M6 9l6 6 6-6"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="member-details">
|
||||
<div class="loading-details">Loading detailed information...</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
logger.debug('ClusterMembersComponent: Setting HTML, length:', membersHTML.length);
|
||||
this.setHTML('', membersHTML);
|
||||
logger.debug('ClusterMembersComponent: HTML set, setting up member cards...');
|
||||
this.setupMemberCards(filteredMembers);
|
||||
}
|
||||
|
||||
setupMemberCards(members) {
|
||||
|
||||
Reference in New Issue
Block a user