diff --git a/ui/src/app/pages/datasets/policies.agent/list/agent.policy.list.component.html b/ui/src/app/pages/datasets/policies.agent/list/agent.policy.list.component.html index aa459edda..50247cd14 100644 --- a/ui/src/app/pages/datasets/policies.agent/list/agent.policy.list.component.html +++ b/ui/src/app/pages/datasets/policies.agent/list/agent.policy.list.component.html @@ -1,4 +1,4 @@ -
+
All Policies class="orb orb-table" [selected]="selected" [selectionType]="'checkbox'" + (tableContextmenu)="onTableContextMenu($event)" > + +
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 68d834553..62f82a5cb 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 @@ -85,6 +85,19 @@ export class AgentPolicyListComponent filters$!: Observable; filteredPolicies$: Observable; + contextMenuRow: any; + + showContextMenu = false; + menuPositionLeft: number; + menuPositionTop: number; + + policyContextMenu = [ + {icon: 'search-outline', action: 'openview'}, + {icon:'edit-outline', action: 'openview'}, + {icon: 'copy-outline', action: 'openduplicate'}, + {icon: 'trash-outline', action: 'opendelete'}, + ]; + constructor( private cdr: ChangeDetectorRef, private dialogService: NbDialogService, @@ -152,6 +165,25 @@ export class AgentPolicyListComponent ); } + onTableContextMenu(event) { + event.event.preventDefault(); + event.event.stopPropagation(); + if (event.type === 'body') { + this.contextMenuRow = { + objectType: 'policy', + ...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.html b/ui/src/app/pages/fleet/agents/list/agent.list.component.html index adf5a4a5d..e3821b3f5 100644 --- a/ui/src/app/pages/fleet/agents/list/agent.list.component.html +++ b/ui/src/app/pages/fleet/agents/list/agent.list.component.html @@ -1,4 +1,4 @@ -
+
+
; private currentComponentWidth; + contextMenuRow: any; + + showContextMenu = false; + menuPositionLeft: number; + menuPositionTop: number; + + agentContextMenu = [ + {icon: 'search-outline', action: 'openview'}, + {icon:'edit-outline', action: 'openview'}, + {icon: 'trash-outline', action: 'opendelete'}, + ]; constructor( private cdr: ChangeDetectorRef, private dialogService: NbDialogService, @@ -191,6 +202,25 @@ export class AgentListComponent implements AfterViewInit, AfterViewChecked, OnDe } } + onTableContextMenu(event) { + event.event.preventDefault(); + event.event.stopPropagation(); + if (event.type === 'body') { + this.contextMenuRow = { + objectType: 'agent', + ...event.content + } + this.menuPositionLeft = event.event.clientX; + this.menuPositionTop = event.event.clientY; + this.showContextMenu = true; + } + } + handleContextClick() { + if (this.showContextMenu) { + this.showContextMenu = false; + } + } + ngAfterViewInit() { this.columns = [ { diff --git a/ui/src/app/pages/fleet/groups/list/agent.group.list.component.html b/ui/src/app/pages/fleet/groups/list/agent.group.list.component.html index 56c817877..9bce885c6 100644 --- a/ui/src/app/pages/fleet/groups/list/agent.group.list.component.html +++ b/ui/src/app/pages/fleet/groups/list/agent.group.list.component.html @@ -1,4 +1,4 @@ -
+
{{ strings.list.header }} class="orb orb-table" [selected]="selected" [selectionType]="'checkbox'" + (tableContextmenu)="onTableContextMenu($event)" > + +
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 8c1c67e64..a33961385 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 @@ -90,6 +90,18 @@ export class AgentGroupListComponent filters$!: Observable; filteredGroups$: Observable; + contextMenuRow: any; + + showContextMenu = false; + menuPositionLeft: number; + menuPositionTop: number; + + agentGroupContextMenu = [ + {icon: 'search-outline', action: 'openview'}, + {icon:'edit-outline', action: 'openedit'}, + {icon: 'trash-outline', action: 'opendelete'}, + ]; + constructor( private cdr: ChangeDetectorRef, private dialogService: NbDialogService, @@ -142,6 +154,25 @@ export class AgentGroupListComponent ); } + onTableContextMenu(event) { + event.event.preventDefault(); + event.event.stopPropagation(); + if (event.type === 'body') { + this.contextMenuRow = { + objectType: 'group', + ...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 eb3d31a38..f65ac54ed 100644 --- a/ui/src/app/pages/pages.module.ts +++ b/ui/src/app/pages/pages.module.ts @@ -14,6 +14,7 @@ import { NbButtonModule, NbCardModule, NbCheckboxModule, + NbContextMenuModule, NbDialogService, NbFormFieldModule, NbInputModule, @@ -73,6 +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'; @NgModule({ imports: [ @@ -108,6 +110,7 @@ import { PolicyDuplicateComponent } from './datasets/policies.agent/duplicate/ag A11yModule, NbAutocompleteModule, SharedModule, + NbContextMenuModule, ], exports: [ SharedModule, diff --git a/ui/src/app/pages/sinks/list/sink.list.component.html b/ui/src/app/pages/sinks/list/sink.list.component.html index 2e54cbb44..7084e03db 100644 --- a/ui/src/app/pages/sinks/list/sink.list.component.html +++ b/ui/src/app/pages/sinks/list/sink.list.component.html @@ -1,4 +1,4 @@ -
+
{{ strings.list.header }} class="orb orb-table" [selected]="selected" [selectionType]="'checkbox'" + (tableContextmenu)="onTableContextMenu($event)" > + +
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 cb18d5225..ede7a5ab9 100644 --- a/ui/src/app/pages/sinks/list/sink.list.component.ts +++ b/ui/src/app/pages/sinks/list/sink.list.component.ts @@ -85,6 +85,18 @@ export class SinkListComponent implements AfterViewInit, AfterViewChecked, OnDes filters$!: Observable; filteredSinks$: Observable; + contextMenuRow: any; + + showContextMenu: boolean; + menuPositionLeft: number; + menuPositionTop: number; + + sinkContextMenu = [ + {icon: 'search-outline', action: 'openview'}, + {icon:'edit-outline', action: 'openview'}, + {icon: 'trash-outline', action: 'opendelete'}, + ]; + constructor( private cdr: ChangeDetectorRef, private dialogService: NbDialogService, @@ -140,6 +152,7 @@ export class SinkListComponent implements AfterViewInit, AfterViewChecked, OnDes this.filters$, this.filterOptions, ); + this.showContextMenu = false; } ngOnDestroy(): void { @@ -234,6 +247,25 @@ export class SinkListComponent implements AfterViewInit, AfterViewChecked, OnDes ]; } + onTableContextMenu(event) { + event.event.preventDefault(); + event.event.stopPropagation(); + if (event.type === 'body') { + this.contextMenuRow = { + objectType: 'sink', + ...event.content + } + this.menuPositionLeft = event.event.clientX; + this.menuPositionTop = event.event.clientY; + this.showContextMenu = true; + } + } + handleContextClick() { + if (this.showContextMenu) { + this.showContextMenu = false; + } + } + onOpenAdd() { this.router.navigate(['add'], { relativeTo: this.route }); } diff --git a/ui/src/app/shared/components/tableContexMenu/table-context-menu-component.html b/ui/src/app/shared/components/tableContexMenu/table-context-menu-component.html new file mode 100644 index 000000000..50d722f62 --- /dev/null +++ b/ui/src/app/shared/components/tableContexMenu/table-context-menu-component.html @@ -0,0 +1,6 @@ +
+ +
diff --git a/ui/src/app/shared/components/tableContexMenu/table-context-menu-component.scss b/ui/src/app/shared/components/tableContexMenu/table-context-menu-component.scss new file mode 100644 index 000000000..9dba836ff --- /dev/null +++ b/ui/src/app/shared/components/tableContexMenu/table-context-menu-component.scss @@ -0,0 +1,28 @@ +.floating-div { + position: absolute; + transform: translate(3%, -90%); + background-color: #171c30; + padding: 7px 10px; + border-radius: 30px; + border-bottom-left-radius: 0; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.5); + z-index: 9999; +} +.button-action { + border: none; + background-color: transparent; + color: #969fb9; + outline: none; +} +.openview, .openedit, .openduplicate { + transition: color 0.3s ease; + &:hover { + color: #3089fc; + } +} +.opendelete { + transition: color 0.3s ease; + &:hover { + color: #df316f; + } +} 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 new file mode 100644 index 000000000..989e076e9 --- /dev/null +++ b/ui/src/app/shared/components/tableContexMenu/table-context-menu-component.ts @@ -0,0 +1,181 @@ +import { Component, Input } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { NbDialogService } from '@nebular/theme'; +import { AgentGroupsService } from 'app/common/services/agents/agent.groups.service'; +import { AgentPoliciesService } from 'app/common/services/agents/agent.policies.service'; +import { AgentsService } from 'app/common/services/agents/agents.service'; +import { NotificationsService } from 'app/common/services/notifications/notifications.service'; +import { OrbService } from 'app/common/services/orb.service'; +import { SinksService } from 'app/common/services/sinks/sinks.service'; +import { AgentPolicyDeleteComponent } from 'app/pages/datasets/policies.agent/delete/agent.policy.delete.component'; +import { PolicyDuplicateComponent } from 'app/pages/datasets/policies.agent/duplicate/agent.policy.duplicate.confirmation'; +import { AgentDeleteComponent } from 'app/pages/fleet/agents/delete/agent.delete.component'; +import { AgentGroupDeleteComponent } from 'app/pages/fleet/groups/delete/agent.group.delete.component'; +import { AgentGroupDetailsComponent } from 'app/pages/fleet/groups/details/agent.group.details.component'; +import { SinkDeleteComponent } from 'app/pages/sinks/delete/sink.delete.component'; + +@Component({ + selector: 'ngx-table-context-menu', + templateUrl: './table-context-menu-component.html', + styleUrls: ['./table-context-menu-component.scss'] +}) +export class TableContextMenu { + + @Input() + items: any[]; + + @Input() + top: number; + + @Input() + left: number; + + @Input() + rowObject: any; + + constructor( + private router: Router, + private route: ActivatedRoute, + private dialogService: NbDialogService, + private orb: OrbService, + protected agentService: AgentsService, + protected notificationsService: NotificationsService, + private agentGroupsService: AgentGroupsService, + private sinkService: SinksService, + private agentPoliciesService: AgentPoliciesService, + ) { + + } + handleClick(item: any) { + const { action } = item; + if (action === 'openview') { + this.openView(); + } else if (action === 'opendelete') { + this.openDelete(); + } else if (action === 'openduplicate') { + this.openDuplicate(); + } else if (action === 'openedit') { + this.openGroupEdit(); + } + } + + openGroupEdit() { + this.router.navigate([`edit/${this.rowObject.id}`], { + state: { agentGroup: this.rowObject, edit: true }, + relativeTo: this.route, + }); + } + + openView() { + const { objectType, id } = this.rowObject; + if (objectType === 'group') { + this.dialogService + .open(AgentGroupDetailsComponent, { + context: { agentGroup: this.rowObject }, + autoFocus: true, + closeOnEsc: true, + }) + .onClose.subscribe((resp) => { + if (resp) { + this.openGroupEdit(); + } + }); + } + else { + this.router.navigate([`view/${id}`], { + relativeTo: this.route, + }); + } + } + 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, { + context: { name }, + autoFocus: true, + closeOnEsc: true, + }) + .onClose.subscribe((confirm) => { + if (confirm) { + this.agentService.deleteAgent(id).subscribe(deleteCallback); + } + }); + } else if (objectType === 'group') { + this.dialogService + .open(AgentGroupDeleteComponent, { + context: { name }, + autoFocus: true, + closeOnEsc: true, + }) + .onClose.subscribe((confirm) => { + if (confirm) { + this.agentGroupsService.deleteAgentGroup(id).subscribe(deleteCallback); + } + }); + } else if (objectType === 'policy') { + this.dialogService + .open(AgentPolicyDeleteComponent, { + context: { name }, + autoFocus: true, + closeOnEsc: true, + }) + .onClose.subscribe((confirm) => { + if (confirm) { + this.agentPoliciesService.deleteAgentPolicy(id).subscribe(deleteCallback); + } + }); + } else if (objectType === 'sink') { + this.dialogService + .open(SinkDeleteComponent, { + context: { sink: this.rowObject }, + autoFocus: true, + closeOnEsc: true, + }) + .onClose.subscribe((confirm) => { + if (confirm) { + this.sinkService.deleteSink(id).subscribe(deleteCallback); + } + }); + } + } + + openDuplicate() { + const policy = this.rowObject.name; + this.dialogService + .open(PolicyDuplicateComponent, { + context: { policy }, + autoFocus: true, + closeOnEsc: true, + }) + .onClose.subscribe((confirm) => { + if (confirm) { + this.duplicatePolicy(this.rowObject); + } + }); + } + duplicatePolicy(agentPolicy: any) { + this.agentPoliciesService + .duplicateAgentPolicy(agentPolicy.id) + .subscribe((newAgentPolicy) => { + if (newAgentPolicy?.id) { + this.notificationsService.success( + 'Agent Policy Duplicated', + `New Agent Policy Name: ${newAgentPolicy?.name}`, + ); + this.router.navigate([`view/${newAgentPolicy.id}`], { + relativeTo: this.route, + }); + } + }); + } +} diff --git a/ui/src/app/shared/shared.module.ts b/ui/src/app/shared/shared.module.ts index 28621b2c4..618cc5498 100644 --- a/ui/src/app/shared/shared.module.ts +++ b/ui/src/app/shared/shared.module.ts @@ -66,6 +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'; @NgModule({ imports: [ @@ -136,6 +137,7 @@ import { SinkConfigComponent } from './components/orb/sink/sink-config/sink-conf EmptyInputDirective, SinkDetailsComponent, SinkConfigComponent, + TableContextMenu, ], exports: [ ThemeModule, @@ -161,6 +163,7 @@ import { SinkConfigComponent } from './components/orb/sink/sink-config/sink-conf PolicyDetailsComponent, PolicyInterfaceComponent, PolicyDatasetsComponent, + TableContextMenu, GroupedAgentsComponent, PolicyGroupsComponent, PrettyYamlPipe,