feat: filter cluster members by multiple labels
This commit is contained in:
@@ -29,11 +29,8 @@ class ClusterMembersComponent extends Component {
|
||||
// Selection state for highlighting
|
||||
this.selectedMemberIp = null;
|
||||
|
||||
// Filter state
|
||||
this.currentFilter = {
|
||||
labelKey: '',
|
||||
labelValue: ''
|
||||
};
|
||||
// Filter state - now supports multiple active filters
|
||||
this.activeFilters = []; // Array of {labelKey, labelValue} objects
|
||||
this.allMembers = []; // Store unfiltered members
|
||||
}
|
||||
|
||||
@@ -70,9 +67,9 @@ class ClusterMembersComponent extends Component {
|
||||
return Array.from(labelValues).sort();
|
||||
}
|
||||
|
||||
// Filter members based on current filter criteria
|
||||
// Filter members based on current active filters
|
||||
filterMembers(members) {
|
||||
if (!this.currentFilter.labelKey && !this.currentFilter.labelValue) {
|
||||
if (!this.activeFilters || this.activeFilters.length === 0) {
|
||||
return members;
|
||||
}
|
||||
|
||||
@@ -81,36 +78,37 @@ class ClusterMembersComponent extends Component {
|
||||
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;
|
||||
// All active filters must match (AND logic)
|
||||
return this.activeFilters.every(filter => {
|
||||
if (filter.labelKey && filter.labelValue) {
|
||||
// Both key and value specified - exact match
|
||||
return member.labels[filter.labelKey] === filter.labelValue;
|
||||
} else if (filter.labelKey && !filter.labelValue) {
|
||||
// Only key specified - member must have this key
|
||||
return member.labels.hasOwnProperty(filter.labelKey);
|
||||
} else if (!filter.labelKey && filter.labelValue) {
|
||||
// Only value specified - member must have this value for any key
|
||||
return Object.values(member.labels).includes(filter.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);
|
||||
// Get currently filtered members to determine available options
|
||||
const filteredMembers = this.filterMembers(this.allMembers);
|
||||
|
||||
// Extract available label keys from filtered members
|
||||
const availableLabelKeys = this.extractLabelKeys(filteredMembers);
|
||||
|
||||
// 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 => {
|
||||
availableLabelKeys.forEach(key => {
|
||||
const option = document.createElement('option');
|
||||
option.value = key;
|
||||
option.textContent = key;
|
||||
@@ -118,11 +116,10 @@ class ClusterMembersComponent extends Component {
|
||||
});
|
||||
|
||||
// Restore selection if it still exists
|
||||
if (currentKey && labelKeys.includes(currentKey)) {
|
||||
if (currentKey && availableLabelKeys.includes(currentKey)) {
|
||||
keySelect.value = currentKey;
|
||||
} else {
|
||||
keySelect.value = '';
|
||||
this.currentFilter.labelKey = '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,16 +130,16 @@ class ClusterMembersComponent extends Component {
|
||||
this.updateClearButtonState();
|
||||
}
|
||||
|
||||
// Update clear button state based on current filter
|
||||
// Update clear button state based on current active filters
|
||||
updateClearButtonState() {
|
||||
const clearBtn = document.getElementById('clear-filters-btn');
|
||||
if (clearBtn) {
|
||||
const hasFilters = this.currentFilter.labelKey || this.currentFilter.labelValue;
|
||||
const hasFilters = this.activeFilters && this.activeFilters.length > 0;
|
||||
clearBtn.disabled = !hasFilters;
|
||||
logger.debug('ClusterMembersComponent: Clear button state updated:', {
|
||||
hasFilters,
|
||||
disabled: clearBtn.disabled,
|
||||
currentFilter: this.currentFilter
|
||||
activeFilters: this.activeFilters
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -155,9 +152,13 @@ class ClusterMembersComponent extends Component {
|
||||
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);
|
||||
// Get currently filtered members to determine available values
|
||||
const filteredMembers = this.filterMembers(this.allMembers);
|
||||
|
||||
// If a key is selected, show only values for that key from filtered members
|
||||
const selectedKey = document.getElementById('label-key-filter')?.value;
|
||||
if (selectedKey) {
|
||||
const labelValues = this.extractLabelValuesForKey(filteredMembers, selectedKey);
|
||||
labelValues.forEach(value => {
|
||||
const option = document.createElement('option');
|
||||
option.value = value;
|
||||
@@ -170,12 +171,11 @@ class ClusterMembersComponent extends Component {
|
||||
valueSelect.value = currentValue;
|
||||
} else {
|
||||
valueSelect.value = '';
|
||||
this.currentFilter.labelValue = '';
|
||||
}
|
||||
} else {
|
||||
// If no key is selected, show all unique values from all keys
|
||||
// If no key is selected, show all unique values from all keys in filtered members
|
||||
const allValues = new Set();
|
||||
this.allMembers.forEach(member => {
|
||||
filteredMembers.forEach(member => {
|
||||
if (member.labels && typeof member.labels === 'object') {
|
||||
Object.values(member.labels).forEach(value => {
|
||||
allValues.add(value);
|
||||
@@ -196,11 +196,126 @@ class ClusterMembersComponent extends Component {
|
||||
valueSelect.value = currentValue;
|
||||
} else {
|
||||
valueSelect.value = '';
|
||||
this.currentFilter.labelValue = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add a new filter and create a pill
|
||||
addFilter(labelKey, labelValue) {
|
||||
// Check if this filter already exists
|
||||
const existingFilter = this.activeFilters.find(filter =>
|
||||
filter.labelKey === labelKey && filter.labelValue === labelValue
|
||||
);
|
||||
|
||||
if (existingFilter) {
|
||||
logger.debug('ClusterMembersComponent: Filter already exists, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
// Add the new filter
|
||||
this.activeFilters.push({ labelKey, labelValue });
|
||||
|
||||
// Create and add the pill
|
||||
this.createFilterPill(labelKey, labelValue);
|
||||
|
||||
// Update dropdowns and apply filter
|
||||
this.updateFilterDropdowns();
|
||||
this.applyFilter();
|
||||
}
|
||||
|
||||
// Remove a filter and its pill
|
||||
removeFilter(labelKey, labelValue) {
|
||||
// Remove from active filters
|
||||
this.activeFilters = this.activeFilters.filter(filter =>
|
||||
!(filter.labelKey === labelKey && filter.labelValue === labelValue)
|
||||
);
|
||||
|
||||
// Remove the pill from DOM
|
||||
this.removeFilterPill(labelKey, labelValue);
|
||||
|
||||
// Update dropdowns and apply filter
|
||||
this.updateFilterDropdowns();
|
||||
this.applyFilter();
|
||||
}
|
||||
|
||||
// Create a filter pill element
|
||||
createFilterPill(labelKey, labelValue) {
|
||||
const pillsContainer = document.getElementById('filter-pills-container');
|
||||
if (!pillsContainer) return;
|
||||
|
||||
const pill = document.createElement('div');
|
||||
pill.className = 'filter-pill';
|
||||
pill.dataset.labelKey = labelKey;
|
||||
pill.dataset.labelValue = labelValue;
|
||||
|
||||
// Create pill text
|
||||
const pillText = document.createElement('span');
|
||||
pillText.className = 'filter-pill-text';
|
||||
|
||||
if (labelKey && labelValue) {
|
||||
pillText.textContent = `${labelKey}: ${labelValue}`;
|
||||
} else if (labelKey && !labelValue) {
|
||||
pillText.textContent = `${labelKey}: *`;
|
||||
} else if (!labelKey && labelValue) {
|
||||
pillText.textContent = `*: ${labelValue}`;
|
||||
}
|
||||
|
||||
// Create remove button
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'filter-pill-remove';
|
||||
removeBtn.title = 'Remove filter';
|
||||
removeBtn.innerHTML = `
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M18 6L6 18M6 6l12 12"/>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
// Add click handler for remove button
|
||||
removeBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.removeFilter(labelKey, labelValue);
|
||||
});
|
||||
|
||||
// Assemble pill
|
||||
pill.appendChild(pillText);
|
||||
pill.appendChild(removeBtn);
|
||||
|
||||
// Add to container
|
||||
pillsContainer.appendChild(pill);
|
||||
}
|
||||
|
||||
// Remove a filter pill from DOM
|
||||
removeFilterPill(labelKey, labelValue) {
|
||||
const pillsContainer = document.getElementById('filter-pills-container');
|
||||
if (!pillsContainer) return;
|
||||
|
||||
const pill = pillsContainer.querySelector(`[data-label-key="${labelKey}"][data-label-value="${labelValue}"]`);
|
||||
if (pill) {
|
||||
pill.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Clear all filters and pills
|
||||
clearAllFilters() {
|
||||
this.activeFilters = [];
|
||||
|
||||
// Clear all pills
|
||||
const pillsContainer = document.getElementById('filter-pills-container');
|
||||
if (pillsContainer) {
|
||||
pillsContainer.innerHTML = '';
|
||||
}
|
||||
|
||||
// Reset dropdowns
|
||||
const keySelect = document.getElementById('label-key-filter');
|
||||
const valueSelect = document.getElementById('label-value-filter');
|
||||
if (keySelect) keySelect.value = '';
|
||||
if (valueSelect) valueSelect.value = '';
|
||||
|
||||
// Update dropdowns and apply filter
|
||||
this.updateFilterDropdowns();
|
||||
this.applyFilter();
|
||||
}
|
||||
|
||||
// Apply filter and re-render
|
||||
applyFilter() {
|
||||
const filteredMembers = this.filterMembers(this.allMembers);
|
||||
@@ -222,20 +337,34 @@ class ClusterMembersComponent extends Component {
|
||||
|
||||
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();
|
||||
const selectedKey = e.target.value;
|
||||
const selectedValue = valueSelect ? valueSelect.value : '';
|
||||
|
||||
// If both key and value are selected, add as a filter pill
|
||||
if (selectedKey && selectedValue) {
|
||||
this.addFilter(selectedKey, selectedValue);
|
||||
// Reset dropdowns after adding filter
|
||||
keySelect.value = '';
|
||||
valueSelect.value = '';
|
||||
} else if (selectedKey && !selectedValue) {
|
||||
// Only key selected, update value dropdown
|
||||
this.updateValueDropdown();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (valueSelect) {
|
||||
valueSelect.addEventListener('change', (e) => {
|
||||
this.currentFilter.labelValue = e.target.value;
|
||||
this.updateClearButtonState();
|
||||
this.applyFilter();
|
||||
const selectedValue = e.target.value;
|
||||
const selectedKey = keySelect ? keySelect.value : '';
|
||||
|
||||
// If both key and value are selected, add as a filter pill
|
||||
if (selectedKey && selectedValue) {
|
||||
this.addFilter(selectedKey, selectedValue);
|
||||
// Reset dropdowns after adding filter
|
||||
keySelect.value = '';
|
||||
valueSelect.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -244,11 +373,7 @@ class ClusterMembersComponent extends Component {
|
||||
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();
|
||||
this.clearAllFilters();
|
||||
});
|
||||
} else {
|
||||
logger.warn('ClusterMembersComponent: Clear filters button not found');
|
||||
|
||||
Reference in New Issue
Block a user