Skip to content

Commit

Permalink
feat(orb-ui): Multi Select option for tag filters
Browse files Browse the repository at this point in the history
  • Loading branch information
joao-mendonca-encora committed Nov 16, 2023
1 parent 5752bfc commit 906cdd1
Show file tree
Hide file tree
Showing 9 changed files with 147 additions and 43 deletions.
23 changes: 19 additions & 4 deletions ui/src/app/common/interfaces/orb/filter-option.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Observable } from 'rxjs';

export enum FilterTypes {
Input, // string input
AutoComplete,
Tags,
Select, // allows select one option
MultiSelect, // allows select multi options
MultiSelectAsync, // allows select multi options | async
Expand Down Expand Up @@ -47,24 +47,39 @@ export function filterTags(item: any, prop: any, value: any, exact?: any) {
if (!item || typeof item[prop] !== 'object') {
return false;
}
// tag values
const values = Object.entries(item[prop]).map(
(entry) => `${entry[0]}:${entry[1]}`,
);
const filterFn = exact ? filterExact : filterSubstr;
const filterValue = value.replace(' ', '');

return values.reduce((acc, val) => {
acc = acc || filterFn(val, filterValue);
return acc;
}, false);

}

export function filterMultiTags(item: any, prop: any, values: any[], exact?: any) {
if (!item || typeof item[prop] !== 'object') {
return false;
}
const filterFn = exact ? filterExact : filterSubstr;
const itemValues = Object.entries(item[prop]).map(
(entry) => `${entry[0]}:${entry[1]}`,
);
return values.some(filterValue => {
const normalizedFilterValue = filterValue.replace(' ', '');
return itemValues.some(val => filterFn(val, normalizedFilterValue));
});
}

export function filterMultiSelect(item: any, prop: any, values: any, exact?: any) {
return values.reduce((prev, cur) => {
if (exact) {
return item[prop] === cur || prev;
return item[prop] ? item[prop].toString() === cur || prev : false;
} else {
return item[prop].includes(cur) || prev;
return item[prop] ? item[prop].toString().includes(cur) || prev : false;
}
}, false);
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
FilterOption, filterString, filterTags,
FilterTypes,
filterMultiSelect,
filterMultiTags,
} from 'app/common/interfaces/orb/filter-option';
import { AgentPoliciesService } from 'app/common/services/agents/agent.policies.service';
import { FilterService } from 'app/common/services/filter.service';
Expand Down Expand Up @@ -136,19 +137,27 @@ export class AgentPolicyListComponent
prop: 'tags',
autoSuggestion: orb.getPolicyTags(),
filter: filterTags,
type: FilterTypes.AutoComplete,
type: FilterTypes.Tags,
},
{
name: 'MultiTags',
prop: 'tags',
filter: filterMultiTags,
autoSuggestion: orb.getAgentsTags(),
type: FilterTypes.MultiSelect,
},
{
name: 'Version',
prop: 'version',
filter: filterNumber,
type: FilterTypes.Number,
filter: filterMultiSelect,
type: FilterTypes.MultiSelect,
exact: true,
},
{
name: 'Description',
prop: 'description',
filter: filterString,
type: FilterTypes.Input,
filter: filterMultiSelect,
type: FilterTypes.MultiSelect,
},
{
name: 'Usage',
Expand Down
10 changes: 9 additions & 1 deletion ui/src/app/pages/fleet/agents/list/agent.list.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
import {Agent, AgentPolicyAggStates, AgentStates} from 'app/common/interfaces/orb/agent.interface';
import {
filterMultiSelect,
filterMultiTags,
FilterOption, filterString,
filterTags,
FilterTypes,
Expand Down Expand Up @@ -151,7 +152,14 @@ export class AgentListComponent implements AfterViewInit, AfterViewChecked, OnDe
prop: 'combined_tags',
filter: filterTags,
autoSuggestion: orb.getAgentsTags(),
type: FilterTypes.AutoComplete,
type: FilterTypes.Tags,
},
{
name: 'MultiTags',
prop: 'combined_tags',
filter: filterMultiTags,
autoSuggestion: orb.getAgentsTags(),
type: FilterTypes.MultiSelect,
},
{
name: 'Status',
Expand Down
14 changes: 11 additions & 3 deletions ui/src/app/pages/fleet/groups/list/agent.group.list.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
import { AgentGroup } from 'app/common/interfaces/orb/agent.group.interface';
import {
filterMultiSelect,
filterMultiTags,
FilterOption, filterString,
filterTags,
FilterTypes,
Expand Down Expand Up @@ -131,13 +132,20 @@ export class AgentGroupListComponent
prop: 'tags',
filter: filterTags,
autoSuggestion: orb.getGroupsTags(),
type: FilterTypes.AutoComplete,
type: FilterTypes.Tags,
},
{
name: 'MultiTags',
prop: 'tags',
filter: filterMultiTags,
autoSuggestion: orb.getAgentsTags(),
type: FilterTypes.MultiSelect,
},
{
name: 'Description',
prop: 'description',
filter: filterString,
type: FilterTypes.Input,
filter: filterMultiSelect,
type: FilterTypes.MultiSelect,
},
];

Expand Down
14 changes: 11 additions & 3 deletions ui/src/app/pages/sinks/list/sink.list.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
} from '@swimlane/ngx-datatable';
import {
filterMultiSelect,
filterMultiTags,
FilterOption, filterString,
filterTags,
FilterTypes,
Expand Down Expand Up @@ -123,7 +124,14 @@ export class SinkListComponent implements AfterViewInit, AfterViewChecked, OnDes
prop: 'tags',
filter: filterTags,
autoSuggestion: orb.getSinksTags(),
type: FilterTypes.AutoComplete,
type: FilterTypes.Tags,
},
{
name: 'MultiTags',
prop: 'tags',
filter: filterMultiTags,
autoSuggestion: orb.getAgentsTags(),
type: FilterTypes.MultiSelect,
},
{
name: 'Status',
Expand All @@ -144,8 +152,8 @@ export class SinkListComponent implements AfterViewInit, AfterViewChecked, OnDes
{
name: 'Description',
prop: 'description',
filter: filterString,
type: FilterTypes.Input,
filter: filterMultiSelect,
type: FilterTypes.MultiSelect,
},
];

Expand Down
20 changes: 15 additions & 5 deletions ui/src/app/shared/components/filter/filter.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
id="filterInput"
class="apply-filter-input"
nbInput
placeholder="{{currentFilter.name}}"
placeholder="{{ currentFilter.name === 'MultiTags' ? 'Tags' : currentFilter.name}}"
[readonly]="currentFilter?.options"
[(ngModel)]="filterText"
(click)="showOptions = true"
Expand Down Expand Up @@ -66,14 +66,14 @@
<span>{{ option | titlecase }}</span>
</div>
</div>
<div class="filter-menu" *ngIf="optionsMenuType === 'input' && showOptions">
<div class="filter-menu" *ngIf="optionsMenuType === 'tags' && showOptions">
<div
*ngFor="let option of currentFilter?.autoSuggestion | async"
class="apply-filter-option"
(click)="handleClickInput($event, option)"
(click)="handleClickTags($event, option)"
>
<input type="checkbox"
(click)="onCheckboxInput($event, option)"
(click)="onCheckboxTags($event, option)"
[checked]="isOptionSelected(option) | async"
>
<span>{{ option }}</span>
Expand All @@ -90,13 +90,23 @@
class="search-input"
(ngModelChange)="onSearchTextChange()">
</div>
<button
*ngIf="optionsMenuType === 'tags' && currentFilter"
class="multi-tags-button"
(click)="toggleMultiTags()"
>
Use <span style="color: #3089fc;" nbTooltip="Multiple filters will be added using {{ isMultiTags ? 'OR' : 'AND' }} condition. Click to toggle.">
{{ isMultiTags ? 'OR' : 'AND' }}
</span>
Condition
</button>
<div *ngIf="(hasActiveFilters() | async)" class="list-div">
<div class="filters-list">
<div class="filter-list-display">
<ng-container *ngFor="let filter of activeFilters$ | async; let index = index" >
<div class="filter-item" *ngIf="filter.name !== 'Name'">
<div class="filter-name">
<span>{{ filter?.name }}</span>
<span>{{ filter.name === 'MultiTags' ? 'Tags' : filter.name }}</span>
</div>
<div class="filter-param">
<span>{{ filter?.param | paramformatter}}</span>
Expand Down
8 changes: 8 additions & 0 deletions ui/src/app/shared/components/filter/filter.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -287,3 +287,11 @@ mat-chip-list {
.pointer-cursor {
cursor: pointer;
}
.multi-tags-button {
color: #969FB9;
background-color: transparent;
border: none;
outline: none;
font-size: 13px;
margin-left: 5px;
}
70 changes: 54 additions & 16 deletions ui/src/app/shared/components/filter/filter.component.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, HostListener, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ChangeDetectorRef, Component, ElementRef, HostListener, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { MatSelectChange } from '@angular/material/select';
import {
FilterOption,
Expand Down Expand Up @@ -42,6 +42,7 @@ export class FilterComponent implements OnInit, OnDestroy {

selectedFilterParams = [];

isMultiTags = false;

@ViewChild('filtersDisplay') filtersDisplay!: ElementRef;

Expand Down Expand Up @@ -92,7 +93,7 @@ export class FilterComponent implements OnInit, OnDestroy {
}

ngOnInit() {
this.availableFilters = this.availableFilters.filter(filter => filter.name !== 'Name');
this.availableFilters = this.availableFilters.filter(filter => filter.name !== 'Name' && filter.name !== 'MultiTags');
this.searchText = this.filter.searchName || '';
if (this.filter.searchName) {
this.searchText = this.filter.searchName;
Expand Down Expand Up @@ -143,6 +144,7 @@ export class FilterComponent implements OnInit, OnDestroy {

clearAllFilters() {
this.filter.cleanFilters();
this.selectedFilterParams = [];
}

toggleExactMatch() {
Expand All @@ -160,7 +162,7 @@ export class FilterComponent implements OnInit, OnDestroy {
}
handleFilterOptionClick(option: any) {
this.showMenu = false;
this.currentFilter = option;
this.currentFilter = Object.assign({}, option);
const icon = document.querySelector('.icon');
icon.classList.toggle('flipped');

Expand All @@ -170,8 +172,9 @@ export class FilterComponent implements OnInit, OnDestroy {
} else if (this.currentFilter.type === FilterTypes.MultiSelectAsync) {
this.selectedFilterParams = this.filter.getMultiAppliedParams(this.currentFilter.name);
this.optionsMenuType = 'multiselectasync';
} else {
this.optionsMenuType = 'input';
} else if (this.currentFilter.type === FilterTypes.Tags) {
this.optionsMenuType = 'tags';
this.isMultiTags = false;
}
}

Expand Down Expand Up @@ -204,28 +207,63 @@ export class FilterComponent implements OnInit, OnDestroy {
this.filter.addFilter({ ...this.currentFilter, param: this.selectedFilterParams });
}
}

handleClickInput(event: any, param: any) {
toggleMultiTags() {
this.isMultiTags = !this.isMultiTags;
if (this.isMultiTags) {
this.currentFilter.name = 'MultiTags';
this.selectedFilterParams = this.filter.getMultiAppliedParams(this.currentFilter.name);
} else {
this.currentFilter.name = 'Tags';
this.selectedFilterParams = [];
}
}
handleClickTags(event: any, param: any) {
if (event.target.type !== 'checkbox') {
if (this.filter.findFilter(param, this.currentFilter.name) === -1) {
this.filter.addFilter({ ...this.currentFilter, param: param });
if (this.isMultiTags) {
if (!this.selectedFilterParams.find(f => f === param)) {
this.filter.findAndRemove(this.selectedFilterParams, this.currentFilter.name);
this.selectedFilterParams.push(param);
this.filter.addFilter({ ...this.currentFilter, param: this.selectedFilterParams });
}
this.currentFilter = null;
this.selectedFilterParams = [];
this.filterText = '';
} else {
if (this.filter.findFilter(param, this.currentFilter.name) === -1) {
this.filter.addFilter({ ...this.currentFilter, param: param });
}
this.currentFilter = null;
this.filterText = '';
}
this.currentFilter = null;
this.filterText = '';
}
}
onCheckboxInput(event: Event, param: any) {
onCheckboxTags(event: Event, param: any) {
const checkbox = event.target as HTMLInputElement;
const isChecked = checkbox.checked;
const oldParamList = this.selectedFilterParams;

if (isChecked) {
this.filter.addFilter({ ...this.currentFilter, param: param });
if (this.isMultiTags) {
if (isChecked) {
this.selectedFilterParams.push(param);
} else {
this.selectedFilterParams = this.selectedFilterParams.filter((f) => f !== param);
}
this.filter.findAndRemove(oldParamList, this.currentFilter.name);
if (this.selectedFilterParams.length > 0) {
this.filter.addFilter({ ...this.currentFilter, param: this.selectedFilterParams });
}
} else {
this.filter.findAndRemove(param, this.currentFilter.name);
if (isChecked) {
this.filter.addFilter({ ...this.currentFilter, param: param });
} else {
this.filter.findAndRemove(param, this.currentFilter.name);
}
}
}

onAddFilterButton(param: any) {
if (this.currentFilter.type === FilterTypes.MultiSelectAsync) {
const isTags = this.currentFilter.name === 'Tags';
if (!isTags || (isTags && this.isMultiTags)) {
if (this.selectedFilterParams.length > 0) {
this.filter.findAndRemove(this.selectedFilterParams, this.currentFilter.name);
this.selectedFilterParams.push(param);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,18 +95,18 @@ export class SinkDetailsComponent implements OnInit, OnChanges {
const { name, description, backend, tags } = this.sink;
this.formGroup = this.fb.group({
name: [
name,
name,
[
Validators.required,
Validators.pattern('^[a-zA-Z_][a-zA-Z0-9_-]*$'),
Validators.maxLength(64)
]
Validators.required,
Validators.pattern('^[a-zA-Z_][a-zA-Z0-9_-]*$'),
Validators.maxLength(64),
],
],
description: [description, [Validators.maxLength(64)]],
backend: [backend, Validators.required],
});
this.selectedTags = { ...tags };

} else {
this.formGroup = this.fb.group({
name: null,
Expand Down

0 comments on commit 906cdd1

Please sign in to comment.