feat: filter cluster members by multiple labels
This commit is contained in:
56
.cursor/rules/cleancode.mdc
Normal file
56
.cursor/rules/cleancode.mdc
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
---
|
||||||
|
description: Guidelines for writing clean, maintainable, and human-readable code. Apply these rules when writing or reviewing code to ensure consistency and quality.
|
||||||
|
globs:
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
# Clean Code Guidelines
|
||||||
|
|
||||||
|
## Constants Over Magic Numbers
|
||||||
|
- Replace hard-coded values with named constants
|
||||||
|
- Use descriptive constant names that explain the value's purpose
|
||||||
|
- Keep constants at the top of the file or in a dedicated constants file
|
||||||
|
|
||||||
|
## Meaningful Names
|
||||||
|
- Variables, functions, and classes should reveal their purpose
|
||||||
|
- Names should explain why something exists and how it's used
|
||||||
|
- Avoid abbreviations unless they're universally understood
|
||||||
|
|
||||||
|
## Smart Comments
|
||||||
|
- Don't comment on what the code does - make the code self-documenting
|
||||||
|
- Use comments to explain why something is done a certain way
|
||||||
|
- Document APIs, complex algorithms, and non-obvious side effects
|
||||||
|
|
||||||
|
## Single Responsibility
|
||||||
|
- Each function should do exactly one thing
|
||||||
|
- Functions should be small and focused
|
||||||
|
- If a function needs a comment to explain what it does, it should be split
|
||||||
|
|
||||||
|
## DRY (Don't Repeat Yourself)
|
||||||
|
- Extract repeated code into reusable functions
|
||||||
|
- Share common logic through proper abstraction
|
||||||
|
- Maintain single sources of truth
|
||||||
|
|
||||||
|
## Clean Structure
|
||||||
|
- Keep related code together
|
||||||
|
- Organize code in a logical hierarchy
|
||||||
|
- Use consistent file and folder naming conventions
|
||||||
|
|
||||||
|
## Encapsulation
|
||||||
|
- Hide implementation details
|
||||||
|
- Expose clear interfaces
|
||||||
|
- Move nested conditionals into well-named functions
|
||||||
|
|
||||||
|
## Code Quality Maintenance
|
||||||
|
- Refactor continuously
|
||||||
|
- Fix technical debt early
|
||||||
|
- Leave code cleaner than you found it
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
- Write tests before fixing bugs
|
||||||
|
- Keep tests readable and maintainable
|
||||||
|
- Test edge cases and error conditions
|
||||||
|
|
||||||
|
## Version Control
|
||||||
|
- Write clear commit messages
|
||||||
|
- Make small, focused commits
|
||||||
|
- Use meaningful branch names
|
||||||
111
.cursor/rules/gitflow.mdc
Normal file
111
.cursor/rules/gitflow.mdc
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
---
|
||||||
|
description: Gitflow Workflow Rules. These rules should be applied when performing git operations.
|
||||||
|
---
|
||||||
|
# Gitflow Workflow Rules
|
||||||
|
|
||||||
|
## Main Branches
|
||||||
|
|
||||||
|
### main (or master)
|
||||||
|
- Contains production-ready code
|
||||||
|
- Never commit directly to main
|
||||||
|
- Only accepts merges from:
|
||||||
|
- hotfix/* branches
|
||||||
|
- release/* branches
|
||||||
|
- Must be tagged with version number after each merge
|
||||||
|
|
||||||
|
### develop
|
||||||
|
- Main development branch
|
||||||
|
- Contains latest delivered development changes
|
||||||
|
- Source branch for feature branches
|
||||||
|
- Never commit directly to develop
|
||||||
|
|
||||||
|
## Supporting Branches
|
||||||
|
|
||||||
|
### feature/*
|
||||||
|
- Branch from: develop
|
||||||
|
- Merge back into: develop
|
||||||
|
- Naming convention: feature/[issue-id]-descriptive-name
|
||||||
|
- Example: feature/123-user-authentication
|
||||||
|
- Must be up-to-date with develop before creating PR
|
||||||
|
- Delete after merge
|
||||||
|
|
||||||
|
### release/*
|
||||||
|
- Branch from: develop
|
||||||
|
- Merge back into:
|
||||||
|
- main
|
||||||
|
- develop
|
||||||
|
- Naming convention: release/vX.Y.Z
|
||||||
|
- Example: release/v1.2.0
|
||||||
|
- Only bug fixes, documentation, and release-oriented tasks
|
||||||
|
- No new features
|
||||||
|
- Delete after merge
|
||||||
|
|
||||||
|
### hotfix/*
|
||||||
|
- Branch from: main
|
||||||
|
- Merge back into:
|
||||||
|
- main
|
||||||
|
- develop
|
||||||
|
- Naming convention: hotfix/vX.Y.Z
|
||||||
|
- Example: hotfix/v1.2.1
|
||||||
|
- Only for urgent production fixes
|
||||||
|
- Delete after merge
|
||||||
|
|
||||||
|
## Commit Messages
|
||||||
|
|
||||||
|
- Format: `type(scope): description`
|
||||||
|
- Types:
|
||||||
|
- feat: New feature
|
||||||
|
- fix: Bug fix
|
||||||
|
- docs: Documentation changes
|
||||||
|
- style: Formatting, missing semicolons, etc.
|
||||||
|
- refactor: Code refactoring
|
||||||
|
- test: Adding tests
|
||||||
|
- chore: Maintenance tasks
|
||||||
|
|
||||||
|
## Version Control
|
||||||
|
|
||||||
|
### Semantic Versioning
|
||||||
|
- MAJOR version for incompatible API changes
|
||||||
|
- MINOR version for backwards-compatible functionality
|
||||||
|
- PATCH version for backwards-compatible bug fixes
|
||||||
|
|
||||||
|
## Pull Request Rules
|
||||||
|
|
||||||
|
1. All changes must go through Pull Requests
|
||||||
|
2. Required approvals: minimum 1
|
||||||
|
3. CI checks must pass
|
||||||
|
4. No direct commits to protected branches (main, develop)
|
||||||
|
5. Branch must be up to date before merging
|
||||||
|
6. Delete branch after merge
|
||||||
|
|
||||||
|
## Branch Protection Rules
|
||||||
|
|
||||||
|
### main & develop
|
||||||
|
- Require pull request reviews
|
||||||
|
- Require status checks to pass
|
||||||
|
- Require branches to be up to date
|
||||||
|
- Include administrators in restrictions
|
||||||
|
- No force pushes
|
||||||
|
- No deletions
|
||||||
|
|
||||||
|
## Release Process
|
||||||
|
|
||||||
|
1. Create release branch from develop
|
||||||
|
2. Bump version numbers
|
||||||
|
3. Fix any release-specific issues
|
||||||
|
4. Create PR to main
|
||||||
|
5. After merge to main:
|
||||||
|
- Tag release
|
||||||
|
- Merge back to develop
|
||||||
|
- Delete release branch
|
||||||
|
|
||||||
|
## Hotfix Process
|
||||||
|
|
||||||
|
1. Create hotfix branch from main
|
||||||
|
2. Fix the issue
|
||||||
|
3. Bump patch version
|
||||||
|
4. Create PR to main
|
||||||
|
5. After merge to main:
|
||||||
|
- Tag release
|
||||||
|
- Merge back to develop
|
||||||
|
- Delete hotfix branch
|
||||||
@@ -91,7 +91,10 @@
|
|||||||
<select id="label-value-filter" class="filter-select">
|
<select id="label-value-filter" class="filter-select">
|
||||||
<option value="">All Values</option>
|
<option value="">All Values</option>
|
||||||
</select>
|
</select>
|
||||||
<button id="clear-filters-btn" class="clear-filters-btn" title="Clear filters">
|
<div class="filter-pills-container" id="filter-pills-container">
|
||||||
|
<!-- Active filter pills will be dynamically added here -->
|
||||||
|
</div>
|
||||||
|
<button id="clear-filters-btn" class="clear-filters-btn" title="Clear all filters">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
|
||||||
<path d="M18 6L6 18M6 6l12 12"/>
|
<path d="M18 6L6 18M6 6l12 12"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -29,11 +29,8 @@ class ClusterMembersComponent extends Component {
|
|||||||
// Selection state for highlighting
|
// Selection state for highlighting
|
||||||
this.selectedMemberIp = null;
|
this.selectedMemberIp = null;
|
||||||
|
|
||||||
// Filter state
|
// Filter state - now supports multiple active filters
|
||||||
this.currentFilter = {
|
this.activeFilters = []; // Array of {labelKey, labelValue} objects
|
||||||
labelKey: '',
|
|
||||||
labelValue: ''
|
|
||||||
};
|
|
||||||
this.allMembers = []; // Store unfiltered members
|
this.allMembers = []; // Store unfiltered members
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,9 +67,9 @@ class ClusterMembersComponent extends Component {
|
|||||||
return Array.from(labelValues).sort();
|
return Array.from(labelValues).sort();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter members based on current filter criteria
|
// Filter members based on current active filters
|
||||||
filterMembers(members) {
|
filterMembers(members) {
|
||||||
if (!this.currentFilter.labelKey && !this.currentFilter.labelValue) {
|
if (!this.activeFilters || this.activeFilters.length === 0) {
|
||||||
return members;
|
return members;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,36 +78,37 @@ class ClusterMembersComponent extends Component {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If only label key is specified, show all members with that key
|
// All active filters must match (AND logic)
|
||||||
if (this.currentFilter.labelKey && !this.currentFilter.labelValue) {
|
return this.activeFilters.every(filter => {
|
||||||
return member.labels.hasOwnProperty(this.currentFilter.labelKey);
|
if (filter.labelKey && filter.labelValue) {
|
||||||
}
|
// Both key and value specified - exact match
|
||||||
|
return member.labels[filter.labelKey] === filter.labelValue;
|
||||||
// If both key and value are specified, show members with exact match
|
} else if (filter.labelKey && !filter.labelValue) {
|
||||||
if (this.currentFilter.labelKey && this.currentFilter.labelValue) {
|
// Only key specified - member must have this key
|
||||||
return member.labels[this.currentFilter.labelKey] === this.currentFilter.labelValue;
|
return member.labels.hasOwnProperty(filter.labelKey);
|
||||||
}
|
} else if (!filter.labelKey && filter.labelValue) {
|
||||||
|
// Only value specified - member must have this value for any key
|
||||||
// If only value is specified, show members with that value for any key
|
return Object.values(member.labels).includes(filter.labelValue);
|
||||||
if (!this.currentFilter.labelKey && this.currentFilter.labelValue) {
|
}
|
||||||
return Object.values(member.labels).includes(this.currentFilter.labelValue);
|
return true;
|
||||||
}
|
});
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update filter dropdowns with current label data
|
// Update filter dropdowns with current label data
|
||||||
updateFilterDropdowns() {
|
updateFilterDropdowns() {
|
||||||
// Always use unfiltered member list for dropdown options
|
// Get currently filtered members to determine available options
|
||||||
const labelKeys = this.extractLabelKeys(this.allMembers);
|
const filteredMembers = this.filterMembers(this.allMembers);
|
||||||
|
|
||||||
|
// Extract available label keys from filtered members
|
||||||
|
const availableLabelKeys = this.extractLabelKeys(filteredMembers);
|
||||||
|
|
||||||
// Update label key dropdown
|
// Update label key dropdown
|
||||||
const keySelect = document.getElementById('label-key-filter');
|
const keySelect = document.getElementById('label-key-filter');
|
||||||
if (keySelect) {
|
if (keySelect) {
|
||||||
const currentKey = keySelect.value;
|
const currentKey = keySelect.value;
|
||||||
keySelect.innerHTML = '<option value="">All Labels</option>';
|
keySelect.innerHTML = '<option value="">All Labels</option>';
|
||||||
labelKeys.forEach(key => {
|
availableLabelKeys.forEach(key => {
|
||||||
const option = document.createElement('option');
|
const option = document.createElement('option');
|
||||||
option.value = key;
|
option.value = key;
|
||||||
option.textContent = key;
|
option.textContent = key;
|
||||||
@@ -118,11 +116,10 @@ class ClusterMembersComponent extends Component {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Restore selection if it still exists
|
// Restore selection if it still exists
|
||||||
if (currentKey && labelKeys.includes(currentKey)) {
|
if (currentKey && availableLabelKeys.includes(currentKey)) {
|
||||||
keySelect.value = currentKey;
|
keySelect.value = currentKey;
|
||||||
} else {
|
} else {
|
||||||
keySelect.value = '';
|
keySelect.value = '';
|
||||||
this.currentFilter.labelKey = '';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,16 +130,16 @@ class ClusterMembersComponent extends Component {
|
|||||||
this.updateClearButtonState();
|
this.updateClearButtonState();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update clear button state based on current filter
|
// Update clear button state based on current active filters
|
||||||
updateClearButtonState() {
|
updateClearButtonState() {
|
||||||
const clearBtn = document.getElementById('clear-filters-btn');
|
const clearBtn = document.getElementById('clear-filters-btn');
|
||||||
if (clearBtn) {
|
if (clearBtn) {
|
||||||
const hasFilters = this.currentFilter.labelKey || this.currentFilter.labelValue;
|
const hasFilters = this.activeFilters && this.activeFilters.length > 0;
|
||||||
clearBtn.disabled = !hasFilters;
|
clearBtn.disabled = !hasFilters;
|
||||||
logger.debug('ClusterMembersComponent: Clear button state updated:', {
|
logger.debug('ClusterMembersComponent: Clear button state updated:', {
|
||||||
hasFilters,
|
hasFilters,
|
||||||
disabled: clearBtn.disabled,
|
disabled: clearBtn.disabled,
|
||||||
currentFilter: this.currentFilter
|
activeFilters: this.activeFilters
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,9 +152,13 @@ class ClusterMembersComponent extends Component {
|
|||||||
const currentValue = valueSelect.value;
|
const currentValue = valueSelect.value;
|
||||||
valueSelect.innerHTML = '<option value="">All Values</option>';
|
valueSelect.innerHTML = '<option value="">All Values</option>';
|
||||||
|
|
||||||
// If a key is selected, show only values for that key
|
// Get currently filtered members to determine available values
|
||||||
if (this.currentFilter.labelKey) {
|
const filteredMembers = this.filterMembers(this.allMembers);
|
||||||
const labelValues = this.extractLabelValuesForKey(this.allMembers, this.currentFilter.labelKey);
|
|
||||||
|
// 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 => {
|
labelValues.forEach(value => {
|
||||||
const option = document.createElement('option');
|
const option = document.createElement('option');
|
||||||
option.value = value;
|
option.value = value;
|
||||||
@@ -170,12 +171,11 @@ class ClusterMembersComponent extends Component {
|
|||||||
valueSelect.value = currentValue;
|
valueSelect.value = currentValue;
|
||||||
} else {
|
} else {
|
||||||
valueSelect.value = '';
|
valueSelect.value = '';
|
||||||
this.currentFilter.labelValue = '';
|
|
||||||
}
|
}
|
||||||
} else {
|
} 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();
|
const allValues = new Set();
|
||||||
this.allMembers.forEach(member => {
|
filteredMembers.forEach(member => {
|
||||||
if (member.labels && typeof member.labels === 'object') {
|
if (member.labels && typeof member.labels === 'object') {
|
||||||
Object.values(member.labels).forEach(value => {
|
Object.values(member.labels).forEach(value => {
|
||||||
allValues.add(value);
|
allValues.add(value);
|
||||||
@@ -196,11 +196,126 @@ class ClusterMembersComponent extends Component {
|
|||||||
valueSelect.value = currentValue;
|
valueSelect.value = currentValue;
|
||||||
} else {
|
} else {
|
||||||
valueSelect.value = '';
|
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
|
// Apply filter and re-render
|
||||||
applyFilter() {
|
applyFilter() {
|
||||||
const filteredMembers = this.filterMembers(this.allMembers);
|
const filteredMembers = this.filterMembers(this.allMembers);
|
||||||
@@ -222,20 +337,34 @@ class ClusterMembersComponent extends Component {
|
|||||||
|
|
||||||
if (keySelect) {
|
if (keySelect) {
|
||||||
keySelect.addEventListener('change', (e) => {
|
keySelect.addEventListener('change', (e) => {
|
||||||
this.currentFilter.labelKey = e.target.value;
|
const selectedKey = e.target.value;
|
||||||
// When key changes, reset value and update value dropdown
|
const selectedValue = valueSelect ? valueSelect.value : '';
|
||||||
this.currentFilter.labelValue = '';
|
|
||||||
this.updateValueDropdown();
|
// If both key and value are selected, add as a filter pill
|
||||||
this.updateClearButtonState();
|
if (selectedKey && selectedValue) {
|
||||||
this.applyFilter();
|
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) {
|
if (valueSelect) {
|
||||||
valueSelect.addEventListener('change', (e) => {
|
valueSelect.addEventListener('change', (e) => {
|
||||||
this.currentFilter.labelValue = e.target.value;
|
const selectedValue = e.target.value;
|
||||||
this.updateClearButtonState();
|
const selectedKey = keySelect ? keySelect.value : '';
|
||||||
this.applyFilter();
|
|
||||||
|
// 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.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
logger.debug('ClusterMembersComponent: Clear filters button clicked');
|
logger.debug('ClusterMembersComponent: Clear filters button clicked');
|
||||||
this.currentFilter = { labelKey: '', labelValue: '' };
|
this.clearAllFilters();
|
||||||
if (keySelect) keySelect.value = '';
|
|
||||||
if (valueSelect) valueSelect.value = '';
|
|
||||||
this.updateClearButtonState();
|
|
||||||
this.applyFilter();
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
logger.warn('ClusterMembersComponent: Clear filters button not found');
|
logger.warn('ClusterMembersComponent: Clear filters button not found');
|
||||||
|
|||||||
@@ -184,6 +184,64 @@ p {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filter-pills-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 1.5rem;
|
||||||
|
margin: 0 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 0.3rem 0.7rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: rgba(30, 58, 138, 0.35);
|
||||||
|
border: 1px solid rgba(59, 130, 246, 0.4);
|
||||||
|
color: #dbeafe;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-pill:hover {
|
||||||
|
background: rgba(30, 58, 138, 0.45);
|
||||||
|
border-color: rgba(59, 130, 246, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-pill-text {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-pill-remove {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #dbeafe;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-pill-remove:hover {
|
||||||
|
background: rgba(59, 130, 246, 0.3);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-pill-remove svg {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.filter-group {
|
.filter-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user