diff --git a/ui/src/app/common/services/filter.service.ts b/ui/src/app/common/services/filter.service.ts index fb843fe8e..9012cbbbf 100644 --- a/ui/src/app/common/services/filter.service.ts +++ b/ui/src/app/common/services/filter.service.ts @@ -88,8 +88,12 @@ export class FilterService { } } - removeFilterByParam(param: string) { - this.removeFilter(this._filters.findIndex((f) => f.param === param && f.name === 'Name' && f)); + findAndRemove(param: any, name: string) { + this.removeFilter(this._filters.findIndex((f) => f.param === param && f.name === name && f)); + } + + findFilter(param: any, name: string) { + return this._filters.findIndex((f) => f.param === param && f.name === name && f); } // make a decorator out of this? diff --git a/ui/src/app/common/services/orb.service.ts b/ui/src/app/common/services/orb.service.ts index 83307c547..1150cd33c 100644 --- a/ui/src/app/common/services/orb.service.ts +++ b/ui/src/app/common/services/orb.service.ts @@ -159,6 +159,16 @@ export class OrbService implements OnDestroy { ), ); } + getAgentsVersions() { + return this.observe(this.agent.getAllAgents()).pipe( + map((agents) => { + return agents + .map((_agent) => _agent?.agent_metadata?.orb_agent?.version) + .filter(version => version !== undefined) + .filter(this.onlyUnique); + }), + ); + } getGroupsTags() { return this.observe(this.group.getAllAgentGroups()).pipe( @@ -287,6 +297,10 @@ export class OrbService implements OnDestroy { map((sinks) => this.mapTags(sinks)), ); } - + getPolicyTags() { + return this.observe(this.policy.getAllAgentPolicies()).pipe( + map((policies) => this.mapTags(policies)), + ); + } onlyUnique = (value, index, self) => self.indexOf(value) === index; } diff --git a/ui/src/app/pages/datasets/policies.agent/list/agent.policy.list.component.ts b/ui/src/app/pages/datasets/policies.agent/list/agent.policy.list.component.ts index f9b4f7eac..e89335145 100644 --- a/ui/src/app/pages/datasets/policies.agent/list/agent.policy.list.component.ts +++ b/ui/src/app/pages/datasets/policies.agent/list/agent.policy.list.component.ts @@ -93,7 +93,7 @@ export class AgentPolicyListComponent policyContextMenu = [ {icon: 'search-outline', action: 'openview'}, - {icon:'edit-outline', action: 'openview'}, + {icon: 'edit-outline', action: 'openview'}, {icon: 'copy-outline', action: 'openduplicate'}, {icon: 'trash-outline', action: 'opendelete'}, ]; @@ -134,6 +134,7 @@ export class AgentPolicyListComponent { name: 'Tags', prop: 'tags', + autoSuggestion: orb.getPolicyTags(), filter: filterTags, type: FilterTypes.AutoComplete, }, @@ -171,19 +172,19 @@ export class AgentPolicyListComponent if (event.type === 'body') { this.contextMenuRow = { objectType: 'policy', - ...event.content - } + ...event.content, + }; this.menuPositionLeft = event.event.clientX; this.menuPositionTop = event.event.clientY; this.showContextMenu = true; - } + } } handleContextClick() { if (this.showContextMenu) { this.showContextMenu = false; } } - + onOpenDuplicatePolicy(agentPolicy: any) { const policy = agentPolicy.name; this.dialogService diff --git a/ui/src/app/pages/fleet/agents/list/agent.list.component.ts b/ui/src/app/pages/fleet/agents/list/agent.list.component.ts index 2ba115dcf..a790bf42d 100644 --- a/ui/src/app/pages/fleet/agents/list/agent.list.component.ts +++ b/ui/src/app/pages/fleet/agents/list/agent.list.component.ts @@ -101,7 +101,7 @@ export class AgentListComponent implements AfterViewInit, AfterViewChecked, OnDe agentContextMenu = [ {icon: 'search-outline', action: 'openview'}, - {icon:'edit-outline', action: 'openview'}, + {icon: 'edit-outline', action: 'openview'}, {icon: 'trash-outline', action: 'opendelete'}, ]; constructor( @@ -172,6 +172,7 @@ export class AgentListComponent implements AfterViewInit, AfterViewChecked, OnDe prop: 'version', filter: filterString, type: FilterTypes.Input, + autoSuggestion: orb.getAgentsVersions(), }, ]; @@ -208,12 +209,12 @@ export class AgentListComponent implements AfterViewInit, AfterViewChecked, OnDe if (event.type === 'body') { this.contextMenuRow = { objectType: 'agent', - ...event.content - } + ...event.content, + }; this.menuPositionLeft = event.event.clientX; this.menuPositionTop = event.event.clientY; this.showContextMenu = true; - } + } } handleContextClick() { if (this.showContextMenu) { diff --git a/ui/src/app/pages/fleet/groups/list/agent.group.list.component.ts b/ui/src/app/pages/fleet/groups/list/agent.group.list.component.ts index 22c1256a4..8aea4fcdf 100644 --- a/ui/src/app/pages/fleet/groups/list/agent.group.list.component.ts +++ b/ui/src/app/pages/fleet/groups/list/agent.group.list.component.ts @@ -17,6 +17,7 @@ import { import { AgentGroup } from 'app/common/interfaces/orb/agent.group.interface'; import { + filterMultiSelect, FilterOption, filterString, filterTags, FilterTypes, @@ -98,7 +99,7 @@ export class AgentGroupListComponent agentGroupContextMenu = [ {icon: 'search-outline', action: 'openview'}, - {icon:'edit-outline', action: 'openedit'}, + {icon: 'edit-outline', action: 'openedit'}, {icon: 'trash-outline', action: 'opendelete'}, ]; @@ -160,19 +161,19 @@ export class AgentGroupListComponent if (event.type === 'body') { this.contextMenuRow = { objectType: 'group', - ...event.content - } + ...event.content, + }; this.menuPositionLeft = event.event.clientX; this.menuPositionTop = event.event.clientY; this.showContextMenu = true; - } + } } handleContextClick() { if (this.showContextMenu) { this.showContextMenu = false; } } - + ngOnDestroy(): void { if (this.groupsSubscription) { this.groupsSubscription.unsubscribe(); diff --git a/ui/src/app/pages/pages.module.ts b/ui/src/app/pages/pages.module.ts index f65ac54ed..c8254e35e 100644 --- a/ui/src/app/pages/pages.module.ts +++ b/ui/src/app/pages/pages.module.ts @@ -74,7 +74,7 @@ import { PagesComponent } from './pages.component'; import { SinkViewComponent } from './sinks/view/sink.view.component'; import { DeleteSelectedComponent } from 'app/shared/components/delete/delete.selected.component'; import { PolicyDuplicateComponent } from './datasets/policies.agent/duplicate/agent.policy.duplicate.confirmation'; -import { TableContextMenu } from 'app/shared/components/tableContexMenu/table-context-menu-component'; +import { TableContextMenuComponent } from 'app/shared/components/tableContexMenu/table-context-menu-component'; @NgModule({ imports: [ diff --git a/ui/src/app/pages/sinks/list/sink.list.component.ts b/ui/src/app/pages/sinks/list/sink.list.component.ts index b9129a7c3..74b7edf44 100644 --- a/ui/src/app/pages/sinks/list/sink.list.component.ts +++ b/ui/src/app/pages/sinks/list/sink.list.component.ts @@ -93,7 +93,7 @@ export class SinkListComponent implements AfterViewInit, AfterViewChecked, OnDes sinkContextMenu = [ {icon: 'search-outline', action: 'openview'}, - {icon:'edit-outline', action: 'openview'}, + {icon: 'edit-outline', action: 'openview'}, {icon: 'trash-outline', action: 'opendelete'}, ]; @@ -254,12 +254,12 @@ export class SinkListComponent implements AfterViewInit, AfterViewChecked, OnDes if (event.type === 'body') { this.contextMenuRow = { objectType: 'sink', - ...event.content - } + ...event.content, + }; this.menuPositionLeft = event.event.clientX; this.menuPositionTop = event.event.clientY; this.showContextMenu = true; - } + } } handleContextClick() { if (this.showContextMenu) { diff --git a/ui/src/app/shared/components/filter/filter.component.html b/ui/src/app/shared/components/filter/filter.component.html index 808411de4..585752ebe 100644 --- a/ui/src/app/shared/components/filter/filter.component.html +++ b/ui/src/app/shared/components/filter/filter.component.html @@ -1,41 +1,71 @@ - - - {{ option.name }} - - +
+
+ Filter by + +
- - - - - - - - - - - - - - - - - - - - - - - +
+ + +
+ + +
+
+ + {{ option | titlecase }} +
+
+
+
+ + {{ option }} +
+
+
@@ -43,7 +73,7 @@ placeholder="Search by name" [(ngModel)]="searchText" nbInput - class="search-input custom-input" + class="search-input" (ngModelChange)="onSearchTextChange()">
@@ -51,106 +81,10 @@ {{ filter?.exact ? filter.name + ": '" + filter?.param + "'" : filter.name + ': ' + filter?.param }} - - - - - - - - - - - {{ option }} - - - - - - - - - - - - - - - {{ option }} - - - - - - - - - - - {{ option }} - - - - - - - - {{ option | ngxCapitalize }} - - - - - - {{ - selectedFilter?.name - }} - diff --git a/ui/src/app/shared/components/filter/filter.component.scss b/ui/src/app/shared/components/filter/filter.component.scss index 3ea7c590f..57267e08d 100644 --- a/ui/src/app/shared/components/filter/filter.component.scss +++ b/ui/src/app/shared/components/filter/filter.component.scss @@ -3,7 +3,7 @@ width: 100% !important; min-height: 32px !important; max-height: 100% !important; - gap: 5px !important; + gap: 0px !important; margin-bottom: 8px !important; flex-direction: row !important; flex-wrap: wrap !important; @@ -12,13 +12,6 @@ align-items: center !important; } -nb-select, -nb-select.select-button, -.select-button { - min-width: 150px !important; - max-width: 100% !important; -} - .exact-match-button { box-shadow: none !important; box-sizing: border-box; @@ -35,31 +28,6 @@ nb-select.select-button, .partial-match { color: #0c5dc5 !important; } -.section-controls { - //display: contents; - - // display: inline-block; - // display: flex; - // flex-direction: row; - // flex-wrap: nowrap; - // align-content: stretch; - // align-items: center;, - // justify-content: flex-start; - // width: 100%; -} - -// -.section-display { - //display: contents; - //display: flex; - // display: inline-flex; - // flex-direction: row; - // align-content: center; - // align-items: center; - // justify-content: flex-end; - // flex-wrap: wrap; - // width: 100%; -} .small-button { padding: 0 !important; @@ -72,7 +40,7 @@ nb-select.select-button, } mat-chip-list { - width: 100%; + width: 50%; align-self: center; } @@ -109,9 +77,10 @@ mat-chip-list { margin-left: 1rem; } .search-input { - width: 300px !important; - height: 38px !important; + width: 280px !important; + height: 32px !important; padding-left: 35px !important; + border-radius: 0px 2px 2px 0px !important; } .filter-search-container { display: flex; @@ -129,4 +98,135 @@ mat-chip-list { pointer-events: none; color: #8f9bb3; } +.add-filter-button { + position: absolute; + right: 6px; + top: 6px; + width: 20px; + height: 20px; + padding: 2px; + border: none; + display: flex; + align-items: center; + justify-content: center; + outline: none; + background-color: transparent; + transition: color 0.3s ease; + color: #3089fc; + &:disabled { + color: #969fb9; + } +} +.apply-filter-input { + width: 100%; + height: 100%; + border-right: none; + padding-right: 32px !important; + &:hover { + border-color: #969fb9 !important; + } +} +.filter-div { + width: 114px; + height: 32px; + position: relative; +} +.filter-button { + background-color: #232940; + border: solid 1px #969FB9 !important; + border-radius: 4px 0px 0px 4px !important; + color: #969FB9; + width: 100%; + height: 100%; + padding: 0; + border-right: none; + cursor: pointer; + transition: background-color 0.3s ease; + nb-icon { + margin-top: 5px; + margin-left: 6px; + } + span { + float: left; + margin-left: 16px; + font-family: Montserrat; + font-size: 14px; + font-weight: 500; + line-height: 18px; + text-align: left; + padding-top: 1px; + margin-top: 5px; + } + &:hover { + background-color: transparent; + } +} +.icon { + transition: transform 0.3s ease; +} +.flipped { + transform: rotateX(180deg); +} +.filter-menu { + position: absolute; + transform: translate(0, 4px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.5); + z-index: 9999; + width: 100%; + padding: 0; + margin: 0; + max-height: 50vh; + overflow-y: scroll; + overflow-x: hidden; +} +.filter-menu-item { + width: 114px; + height: 37px; + background-color: rgba(35, 41, 64, 0.96); + border: none; + color: #fff; + outline: none; + &:hover { + background-color: rgba(55,64,86, 0.96); + } + span { + font-family: Montserrat; + float: left; + padding-left: 12px; + font-size: 13px; + font-weight: 500; + } +} +.apply-filter-div { + width: 150px; + height: 32px; + position: relative; +} +.apply-filter-option { + display: flex; + align-items: center; + width: 160px; + height: 37px; + background-color: rgba(35, 41, 64, 0.96); + border: none; + color: #fff; + outline: none; + cursor: pointer; + &:hover { + background-color: rgba(55,64,86, 0.96); + } + span { + font-family: Montserrat; + float: left; + padding-left: 15px; + font-size: 13px; + font-weight: 500; + } + input { + margin-left: 10px; + } +} +.pointer-cursor { + cursor: pointer; +} diff --git a/ui/src/app/shared/components/filter/filter.component.ts b/ui/src/app/shared/components/filter/filter.component.ts index 56da43abb..f59b229f7 100644 --- a/ui/src/app/shared/components/filter/filter.component.ts +++ b/ui/src/app/shared/components/filter/filter.component.ts @@ -1,4 +1,4 @@ -import { Component, HostListener, Input, OnInit } from '@angular/core'; +import { Component, ElementRef, HostListener, Input, OnInit, ViewChild } from '@angular/core'; import { MatSelectChange } from '@angular/material/select'; import { FilterOption, @@ -7,7 +7,7 @@ import { } from 'app/common/interfaces/orb/filter-option'; import { FilterService } from 'app/common/services/filter.service'; import { Observable } from 'rxjs'; -import { map, tap } from 'rxjs/operators'; +import { map } from 'rxjs/operators'; @Component({ selector: 'ngx-filter', @@ -20,12 +20,8 @@ export class FilterComponent implements OnInit { activeFilters$: Observable; - type = FilterTypes; - selectedFilter!: FilterOption | null; - filterParam: any; - exact: boolean; searchText: string; @@ -34,12 +30,51 @@ export class FilterComponent implements OnInit { loadedSearchText: string; - constructor(private filter: FilterService) { + filterType = ''; + + showMenu = false; + + currentFilter: FilterOption | null = null; + + filterText: string = ''; + + showOptions = true; + + selectedFiltersParams: FilterOption[] = []; + + @ViewChild('filterMenu') filterMenu: ElementRef; + + constructor( + private filter: FilterService, + private elRef: ElementRef, + ) { this.exact = false; this.availableFilters = []; this.activeFilters$ = filter.getFilters().pipe(map((filters) => filters)); } + @HostListener('document:click', ['$event']) + handleOutsideClick(event: Event) { + const target = event.target as HTMLElement; + const parentId = (target.parentNode as HTMLElement).id; + + if (!this.elRef.nativeElement.contains(event.target) && parentId !== 'filterMenu' && parentId !== 'remove-button') { + if (this.currentFilter) { + if (this.showOptions) { + this.showOptions = false; + } else { + this.currentFilter = null; + this.selectedFiltersParams = []; + } + } + if (this.showMenu) { + const icon = document.querySelector('.icon'); + icon.classList.toggle('flipped'); + } + this.showMenu = false; + } + } + ngOnInit() { this.availableFilters = this.availableFilters.filter(filter => filter.name !== 'Name'); this.searchText = this.filter.searchName || ''; @@ -52,11 +87,11 @@ export class FilterComponent implements OnInit { } onSearchTextChange() { if (this.loadedSearchText) { - this.filter.removeFilterByParam(this.loadedSearchText); + this.filter.findAndRemove(this.loadedSearchText, 'Name'); this.loadedSearchText = undefined; } if (this.lastSearchText !== '') { - this.filter.removeFilterByParam(this.lastSearchText); + this.filter.findAndRemove(this.lastSearchText, 'Name'); } if (this.searchText !== '') { const filterOptions: FilterOption = { @@ -70,21 +105,6 @@ export class FilterComponent implements OnInit { } this.lastSearchText = this.searchText; } - addFilter(): void { - if (!this.selectedFilter) return; - - this.filter.addFilter({ ...this.selectedFilter, param: this.filterParam }); - - this.selectedFilter = null; - this.filterParam = null; - } - - @HostListener('window:keydown.enter', ['$event']) - handleKeyDown(event: KeyboardEvent) { - if (event.key === 'Enter' && this.filterParam) { - this.addFilter(); - } - } removeFilter(index: number) { this.filter.removeFilter(index); @@ -101,4 +121,84 @@ export class FilterComponent implements OnInit { toggleExactMatch() { this.selectedFilter.exact = !this.selectedFilter.exact; } + + onFilterClick() { + this.showMenu = !this.showMenu; + this.currentFilter = null; + this.selectedFiltersParams = []; + const icon = document.querySelector('.icon'); + icon.classList.toggle('flipped'); + this.showOptions = true; + } + handleFilterOptionClick(option: any) { + this.showMenu = false; + this.filterType = option.filter.name; + this.currentFilter = option; + const icon = document.querySelector('.icon'); + icon.classList.toggle('flipped'); + } + onCheckboxMultiSelect(event: Event, param: any) { + const checkbox = event.target as HTMLInputElement; + const isChecked = checkbox.checked; + const oldParamList = this.selectedFiltersParams; + + if (isChecked) { + this.selectedFiltersParams.push(param); + } else { + this.selectedFiltersParams = this.selectedFiltersParams.filter((f) => f !== param); + } + + this.filter.findAndRemove(oldParamList, this.currentFilter.name); + if (this.selectedFiltersParams.length > 0) { + this.filter.addFilter({ ...this.currentFilter, param: this.selectedFiltersParams }); + } + } + + handleClickMultiSelect(event: any, param: any) { + if (event.target.type !== 'checkbox') { + this.addFilterClick(param); + } + } + addFilterClick(param: any) { + if (this.selectedFiltersParams.length === 1 && this.selectedFiltersParams[0] === param) { + + } else if (this.selectedFiltersParams.length >= 1) { + this.filter.findAndRemove(this.selectedFiltersParams, this.currentFilter.name); + this.selectedFiltersParams.push(param); + this.filter.addFilter({ ...this.currentFilter, param: this.selectedFiltersParams }); + } else { + const params = []; + params.push(param); + this.filter.addFilter({ ...this.currentFilter, param: params }); + } + this.currentFilter = null; + this.selectedFiltersParams = []; + } + applyFilter(event: any, param: any) { + if (event.target.type !== 'checkbox') { + this.filter.addFilter({ ...this.currentFilter, param: param }); + this.currentFilter = null; + } + } + onCheckboxApply(event: Event, param: any) { + const checkbox = event.target as HTMLInputElement; + const isChecked = checkbox.checked; + + if (isChecked) { + this.filter.addFilter({ ...this.currentFilter, param: param }); + } else { + this.filter.findAndRemove(param, this.currentFilter.name); + } + } + onAddFilterButton(param: any) { + this.filter.addFilter({ ...this.currentFilter, param: param }); + this.filterText = ''; + } + isOptionSelected(option: any) { + return this.activeFilters$.pipe( + map(filters => { + return filters.some(filter => filter.name === this.currentFilter.name && filter.param === option); + }), + ); + } } diff --git a/ui/src/app/shared/components/tableContexMenu/table-context-menu-component.ts b/ui/src/app/shared/components/tableContexMenu/table-context-menu-component.ts index 989e076e9..f9db4d635 100644 --- a/ui/src/app/shared/components/tableContexMenu/table-context-menu-component.ts +++ b/ui/src/app/shared/components/tableContexMenu/table-context-menu-component.ts @@ -17,9 +17,9 @@ import { SinkDeleteComponent } from 'app/pages/sinks/delete/sink.delete.componen @Component({ selector: 'ngx-table-context-menu', templateUrl: './table-context-menu-component.html', - styleUrls: ['./table-context-menu-component.scss'] + styleUrls: ['./table-context-menu-component.scss'], }) -export class TableContextMenu { +export class TableContextMenuComponent { @Input() items: any[]; @@ -80,8 +80,7 @@ export class TableContextMenu { this.openGroupEdit(); } }); - } - else { + } else { this.router.navigate([`view/${id}`], { relativeTo: this.route, }); @@ -89,15 +88,15 @@ export class TableContextMenu { } openDelete() { const { objectType, name, id } = this.rowObject; - + const deleteCallback = () => { this.notificationsService.success( `${objectType.charAt(0).toUpperCase() + objectType.slice(1)} successfully deleted`, - '' + '', ); this.orb.refreshNow(); }; - + if (objectType === 'agent') { this.dialogService .open(AgentDeleteComponent, { diff --git a/ui/src/app/shared/pipes/list-json.pipe.ts b/ui/src/app/shared/pipes/list-json.pipe.ts index 672942be5..1b80623b9 100644 --- a/ui/src/app/shared/pipes/list-json.pipe.ts +++ b/ui/src/app/shared/pipes/list-json.pipe.ts @@ -2,7 +2,7 @@ import { Pipe, PipeTransform } from '@angular/core'; @Pipe({name: 'jsonlist'}) export class JsonListPipe implements PipeTransform { - + transform(object: any): string { if (!object || typeof object !== 'object') { return ''; @@ -11,4 +11,4 @@ export class JsonListPipe implements PipeTransform { const formattedList = entries.map(([key, value]) => `${key}: ${value}`).join(', '); return formattedList; } -} \ No newline at end of file +} diff --git a/ui/src/app/shared/shared.module.ts b/ui/src/app/shared/shared.module.ts index a521d005d..c3ffe2355 100644 --- a/ui/src/app/shared/shared.module.ts +++ b/ui/src/app/shared/shared.module.ts @@ -66,7 +66,7 @@ import {EmptyInputDirective} from 'app/shared/directives/empty-input.directive'; import { AgentBackendsComponent } from './components/orb/agent/agent-backends/agent-backends.component'; import { SinkDetailsComponent } from './components/orb/sink/sink-details/sink-details.component'; import { SinkConfigComponent } from './components/orb/sink/sink-config/sink-config.component'; -import { TableContextMenu } from './components/tableContexMenu/table-context-menu-component'; +import { TableContextMenuComponent } from './components/tableContexMenu/table-context-menu-component'; import { JsonListPipe } from './pipes/list-json.pipe'; @NgModule({ @@ -139,7 +139,7 @@ import { JsonListPipe } from './pipes/list-json.pipe'; EmptyInputDirective, SinkDetailsComponent, SinkConfigComponent, - TableContextMenu, + TableContextMenuComponent, ], exports: [ ThemeModule, @@ -166,7 +166,7 @@ import { JsonListPipe } from './pipes/list-json.pipe'; PolicyDetailsComponent, PolicyInterfaceComponent, PolicyDatasetsComponent, - TableContextMenu, + TableContextMenuComponent, GroupedAgentsComponent, PolicyGroupsComponent, PrettyYamlPipe,