From 9f43ae7131423a5eb2a56aa73cf74e04f611ded8 Mon Sep 17 00:00:00 2001 From: Armin Date: Mon, 9 Sep 2024 12:02:55 +0330 Subject: [PATCH] feat(graph): Add search (#70) Co-authored-by: Armin Co-authored-by: Arash-Azarpoor --- .../data-overview-drawer.component.html | 6 +- .../data-overview-drawer.component.spec.ts | 4 +- .../data-overview-drawer.component.ts | 61 ++++++++++++- .../sigma/sigma/sigma.component.ts | 91 +++++++++++++------ .../graph-tool-bar.component.html | 28 +++--- .../graph-tool-bar.component.scss | 13 +++ .../graph-tool-bar.component.ts | 21 ++++- src/app/models/graph-category.ts | 6 +- src/app/models/graph-records.ts | 1 + src/app/models/search-graph-node.ts | 8 ++ src/app/services/graph/graph.service.spec.ts | 8 +- src/app/services/graph/graph.service.ts | 9 +- src/app/services/sigma/sigma.service.ts | 45 ++++++++- 13 files changed, 236 insertions(+), 65 deletions(-) create mode 100644 src/app/models/search-graph-node.ts diff --git a/src/app/components/graph-components/data-overview-drawer/data-overview-drawer.component.html b/src/app/components/graph-components/data-overview-drawer/data-overview-drawer.component.html index c6aff4f..c42a3fb 100644 --- a/src/app/components/graph-components/data-overview-drawer/data-overview-drawer.component.html +++ b/src/app/components/graph-components/data-overview-drawer/data-overview-drawer.component.html @@ -8,10 +8,12 @@ >
- + - +
diff --git a/src/app/components/graph-components/data-overview-drawer/data-overview-drawer.component.spec.ts b/src/app/components/graph-components/data-overview-drawer/data-overview-drawer.component.spec.ts index 33d9d57..eac7a44 100644 --- a/src/app/components/graph-components/data-overview-drawer/data-overview-drawer.component.spec.ts +++ b/src/app/components/graph-components/data-overview-drawer/data-overview-drawer.component.spec.ts @@ -1,6 +1,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { DataOverviewDrawerComponent } from './data-overview-drawer.component'; +import { provideHttpClient } from '@angular/common/http'; describe('DataOverviewDrawerComponent', () => { let component: DataOverviewDrawerComponent; @@ -8,7 +9,8 @@ describe('DataOverviewDrawerComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [DataOverviewDrawerComponent] + imports: [DataOverviewDrawerComponent], + providers: [provideHttpClient()] }) .compileComponents(); diff --git a/src/app/components/graph-components/data-overview-drawer/data-overview-drawer.component.ts b/src/app/components/graph-components/data-overview-drawer/data-overview-drawer.component.ts index bf6489e..2c284cf 100644 --- a/src/app/components/graph-components/data-overview-drawer/data-overview-drawer.component.ts +++ b/src/app/components/graph-components/data-overview-drawer/data-overview-drawer.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { AfterViewInit, Component, EventEmitter, Input, Output } from '@angular/core'; import { NzDropdownMenuComponent, NzDropDownModule } from 'ng-zorro-antd/dropdown'; import { NzDrawerModule } from 'ng-zorro-antd/drawer'; import { NzInputModule } from 'ng-zorro-antd/input'; @@ -9,6 +9,12 @@ import { NzBadgeComponent } from 'ng-zorro-antd/badge'; import { nodeData } from '../../../models/node-data'; import { edgeData } from '../../../models/edge-data'; import { NzIconModule } from 'ng-zorro-antd/icon'; +import { GraphService } from '../../../services/graph/graph.service'; +import { SigmaService } from '../../../services/sigma/sigma.service'; +import { graphCategory } from '../../../models/graph-category'; +import { FormsModule } from '@angular/forms'; +import { searchGraphNode } from '../../../models/search-graph-node'; +import { NotificationService } from '../../../services/notification/notification.service'; @Component({ selector: 'app-data-overview-drawer', @@ -23,12 +29,16 @@ import { NzIconModule } from 'ng-zorro-antd/icon'; NzBadgeComponent, NzIconModule, NzDropdownMenuComponent, - NzDropDownModule + NzDropDownModule, + FormsModule, ], templateUrl: './data-overview-drawer.component.html', - styleUrl: './data-overview-drawer.component.scss' + styleUrl: './data-overview-drawer.component.scss', }) -export class DataOverviewDrawerComponent { +export class DataOverviewDrawerComponent implements AfterViewInit { + protected searchTerm = ''; + private selectedCategories!: graphCategory; + @Input() visible = false; @Input() selectedNode: nodeData | null = null; @Input() selectedEdge: edgeData | null = null; @@ -37,10 +47,45 @@ export class DataOverviewDrawerComponent { @Output() closeDrawer = new EventEmitter(); + constructor(private graphService: GraphService, private sigmaService: SigmaService, private notificationService: NotificationService) {} + + ngAfterViewInit(): void { + this.subsctibeToServices(); + } + close(): void { this.closeDrawer.emit(); } + search(): void { + console.log(this.selectedCategories) + const data: searchGraphNode = { + sourceCategoryName: this.selectedCategories.SourceNodeCategoryName, + targetCategoryName: this.selectedCategories.TargetNodeCategoryName, + edgeCategoryName: this.selectedCategories.EdgeCategoryName, + sourceCategoryClauses: { + AccountID: this.searchTerm, + }, + targetCategoryClauses: { + AccountID: this.searchTerm, + }, + edgeCategoryClauses: {}, + }; + + this.graphService.searchNode(data).subscribe({ + next: (data) => { + if (data.nodes.length === 0) { + this.notificationService.createNotification('info', 'Info', 'No results found'); + return; + } + this.sigmaService.setGetGraph(data); + }, + error: (error) => { + this.notificationService.createNotification('error', 'Error', error.message); + } + }); + } + expand(): void { // Logic for expanding the node console.log('Expand clicked for node:', this.selectedNodeId); @@ -50,4 +95,12 @@ export class DataOverviewDrawerComponent { // Logic for deleting the node console.log('Delete clicked for node:', this.selectedNodeId); } + + subsctibeToServices() { + this.sigmaService.selectedGraphCategories$.subscribe({ + next: (data) => { + this.selectedCategories = data + }, + }); + } } diff --git a/src/app/components/graph-components/sigma/sigma/sigma.component.ts b/src/app/components/graph-components/sigma/sigma/sigma.component.ts index 45b2d75..3bf2521 100644 --- a/src/app/components/graph-components/sigma/sigma/sigma.component.ts +++ b/src/app/components/graph-components/sigma/sigma/sigma.component.ts @@ -45,12 +45,15 @@ export class SigmaComponent implements AfterViewInit { private state: State = { searchQuery: '' }; private cancelCurrentAnimation: (() => void) | null = null; private nodesList: GraphNode[] = []; + private renderEdgeLabel = true; + private toggleHover = false; protected drawerVisible = false; protected selectedNode: nodeData | null = null; protected selectedEdge: edgeData | null = null; protected selectedNodeId!: string; protected selectedEdgeId!: string; protected selectedCategories!: graphCategory; + constructor( private sigmaService: SigmaService, @@ -88,15 +91,15 @@ export class SigmaComponent implements AfterViewInit { this.addDragNodeFuntionality(); + + this.sigmaInstance.getMouseCaptor().on('mousedown', () => { if (!this.sigmaInstance.getCustomBBox()) this.sigmaInstance.setCustomBBox(this.sigmaInstance.getBBox()); }); this.handleLeaveNode(); - this.nodeSetting(); - - this.setReducerSetting(); + } protected resetCamera() { @@ -196,13 +199,16 @@ export class SigmaComponent implements AfterViewInit { } private addEdges(edges: { id: string; source: string; target: string }[]) { - const attr = { - label: 'test', - size: 10, - }; + edges.forEach((edge: { id: string; source: string; target: string }) => { + const attr = { + id: edge.id, + label: 'test', + size: 10, + }; this.graph.addEdge(edge.source, edge.target, attr); }); + } private addDragNodeFuntionality() { @@ -271,7 +277,6 @@ export class SigmaComponent implements AfterViewInit { } private expandNode(id: string, neighbors: graphRecords) { - console.log(id, neighbors); const centerCordinate = { x: this.graph.getNodeAttribute(id, 'x'), y: this.graph.getNodeAttribute(id, 'y'), @@ -279,6 +284,7 @@ export class SigmaComponent implements AfterViewInit { const newPositions: PlainObject> = {}; neighbors.nodes.forEach((neighbour, index) => { + if (!this.graph.hasNode(neighbour.id)) { this.graph.addNode(neighbour.id, { label: neighbour.label, x: centerCordinate.x, @@ -297,8 +303,7 @@ export class SigmaComponent implements AfterViewInit { newPositions[neighbour.id] = { x: newX, y: newY, - }; - console.log(newPositions); + };} }); this.addEdges(neighbors.edges); @@ -311,7 +316,7 @@ export class SigmaComponent implements AfterViewInit { allowInvalidContainer: true, enableEdgeEvents: true, defaultEdgeType: 'curved', - renderEdgeLabels: true, + renderEdgeLabels: this.renderEdgeLabel, edgeProgramClasses: { straight: EdgeArrowProgram, curved: EdgeCurvedArrowProgram, @@ -325,6 +330,20 @@ export class SigmaComponent implements AfterViewInit { this.circularLayout(); }); + this.sigmaService.renderEdgeLabel$.subscribe((data)=>{ + this.renderEdgeLabel = data; + this.sigmaInstance.setSetting('renderEdgeLabels', this.renderEdgeLabel); + this.sigmaInstance.refresh(); + }) + + this.sigmaService.toggleHover$.subscribe((data)=>{ + this.toggleHover = data + + if(data){ + this.handleEnterNode() + } + }) + this.sigmaService.randomLayoutTrigger$.subscribe(() => { this.randomLayout(); }); @@ -340,7 +359,8 @@ export class SigmaComponent implements AfterViewInit { this.sigmaService.getGraph$.subscribe((data) => { const nodes = data['nodes']; const edges = data['edges']; - + this.nodesList = []; + nodes.forEach((element: { id: string; label: string }) => { this.nodesList.push({ id: element.id, @@ -352,7 +372,8 @@ export class SigmaComponent implements AfterViewInit { expanded: true, }); }); - + this.graph.clear(); + this.sigmaInstance.refresh(); this.addNodes(this.nodesList); this.addEdges(edges); }); @@ -381,8 +402,8 @@ export class SigmaComponent implements AfterViewInit { private edgeClickHandler() { this.sigmaInstance.on('clickEdge', (event) => { this.selectedEdgeId = (parseInt(event.edge.charAt(event.edge.length - 1)) + 1).toString(); - - this.uploadService.getEdgeById(event.edge.charAt(event.edge.length - 1)).subscribe({ + + this.uploadService.getEdgeById(this.graph.getEdgeAttributes(event.edge)['id']).subscribe({ next: (data) => { this.selectedEdge = data; }, @@ -394,16 +415,18 @@ export class SigmaComponent implements AfterViewInit { private doubleClickHandler() { this.sigmaInstance.on('doubleClickNode', (event) => { + let neighborData : graphRecords = {nodes: [] , edges:[]} event.preventSigmaDefault(); if (this.graph.getNodeAttribute(event.node, 'expanded')) { - console.log('its expanded'); this.collapseNode(event.node); } else { - console.log('it is not expandded'); this.mockBack.getNeighbourById(event.node).subscribe((data) => { - this.expandNode(event.node, data); + neighborData = data + }); + this.expandNode(event.node, neighborData); } + }); this.sigmaInstance.on('doubleClickStage', (e) => { e.preventSigmaDefault(); @@ -429,8 +452,14 @@ export class SigmaComponent implements AfterViewInit { private handleEnterNode() { this.sigmaInstance.on('enterNode', ({ node }) => { - this.setHoveredNode(node); + if (this.toggleHover) { + this.setHoveredNode(node); + } }); + + this.nodeSetting(); + + this.setReducerSetting(); } private handleLeaveNode() { @@ -469,24 +498,28 @@ export class SigmaComponent implements AfterViewInit { } collapseNode(id: string) { - console.log(`we gonna collapse ${id}`); - const centerCordinate = { x: this.graph.getNodeAttribute(id, 'x'), y: this.graph.getNodeAttribute(id, 'y'), }; const newPositions: PlainObject> = {}; const neighbours = this.graph.neighbors(id); + neighbours.forEach((neighbour) => { - newPositions[neighbour] = { - x: centerCordinate.x, - y: centerCordinate.y, - }; - - setTimeout(() => { - this.graph.dropNode(neighbour); - }, 550); + const neighborNeighbors = this.graph.neighbors(neighbour); + const hasOnlyClickedNodeAsNeighbor = neighborNeighbors.length === 1 && neighborNeighbors[0] === id; + if (hasOnlyClickedNodeAsNeighbor) { + newPositions[neighbour] = { + x: centerCordinate.x, + y: centerCordinate.y, + }; + + setTimeout(() => { + this.graph.dropNode(neighbour); + }, 550); + } + }); this.graph.setNodeAttribute(id, 'expanded', false); diff --git a/src/app/components/graph-components/toolbarl/graph-tool-bar/graph-tool-bar.component.html b/src/app/components/graph-components/toolbarl/graph-tool-bar/graph-tool-bar.component.html index 30527e8..38b5695 100644 --- a/src/app/components/graph-components/toolbarl/graph-tool-bar/graph-tool-bar.component.html +++ b/src/app/components/graph-components/toolbarl/graph-tool-bar/graph-tool-bar.component.html @@ -4,7 +4,7 @@ nzPopoverTrigger="click" nz-popover nz-tooltip - nzTooltipTitle="Upload" + nzTooltipTitle="Upload" nzPopoverTitle="Upload" [nzPopoverContent]="upload" nzPopoverPlacement="bottom" @@ -20,7 +20,7 @@ nzPopoverTrigger="click" nz-popover nz-tooltip - nzTooltipTitle="Get Graph" + nzTooltipTitle="Get Graph" nzPopoverTitle="Get Graph" [nzPopoverContent]="getGgraph" nzPopoverPlacement="bottom" @@ -36,7 +36,7 @@ nzPopoverTrigger="click" nz-popover nz-tooltip - nzTooltipTitle="Layouts" + nzTooltipTitle="Layouts" nzPopoverTitle="Layouts" [nzPopoverContent]="layouts" nzPopoverPlacement="bottom" @@ -53,7 +53,7 @@ nzPopoverTrigger="click" nz-popover nz-tooltip - nzTooltipTitle="Settings" + nzTooltipTitle="Settings" nzPopoverTitle="Settings" [nzPopoverContent]="Options" nzPopoverPlacement="bottom" @@ -68,7 +68,7 @@
@@ -86,13 +86,17 @@ -
-

- Toggle show connected nodes on hover - -

-

More options to be added

+
+
+

Toggle show connected nodes on hover

+ +
+
+

Toggle edge label

+ +
+ diff --git a/src/app/components/graph-components/toolbarl/graph-tool-bar/graph-tool-bar.component.scss b/src/app/components/graph-components/toolbarl/graph-tool-bar/graph-tool-bar.component.scss index e2c31e4..1998626 100644 --- a/src/app/components/graph-components/toolbarl/graph-tool-bar/graph-tool-bar.component.scss +++ b/src/app/components/graph-components/toolbarl/graph-tool-bar/graph-tool-bar.component.scss @@ -78,4 +78,17 @@ .grow-div{ flex-grow: 1; +} + +.option{ + display: flex; + width: 100%; + justify-content: space-between; + gap: 1rem; +} + +.option-container{ + display: flex; + flex-direction: column; + gap: 1.5rem; } \ No newline at end of file diff --git a/src/app/components/graph-components/toolbarl/graph-tool-bar/graph-tool-bar.component.ts b/src/app/components/graph-components/toolbarl/graph-tool-bar/graph-tool-bar.component.ts index 53a62fb..856b5f2 100644 --- a/src/app/components/graph-components/toolbarl/graph-tool-bar/graph-tool-bar.component.ts +++ b/src/app/components/graph-components/toolbarl/graph-tool-bar/graph-tool-bar.component.ts @@ -10,7 +10,8 @@ import { LayoutsComponent } from '../toolbar-components/layouts/layouts.componen import { UploadComponentsComponent } from '../toolbar-components/upload-component/upload-layout/upload-components.component'; import { NzSwitchModule } from 'ng-zorro-antd/switch'; import { RouterLink } from '@angular/router'; -import { GetGraphComponent } from "../toolbar-components/get-graph/get-graph.component"; +import { GetGraphComponent } from '../toolbar-components/get-graph/get-graph.component'; +import { FormsModule } from '@angular/forms'; @Component({ selector: 'app-graph-tool-bar', @@ -26,13 +27,17 @@ import { GetGraphComponent } from "../toolbar-components/get-graph/get-graph.com LayoutsComponent, UploadComponentsComponent, NzSwitchModule, - GetGraphComponent -], + GetGraphComponent, + FormsModule, + ], providers: [], templateUrl: './graph-tool-bar.component.html', styleUrl: './graph-tool-bar.component.scss', }) export class GraphToolBarComponent { + protected isSwitchChecked = true; + protected hoverToggle = false; + constructor(private sigmaService: SigmaService) {} activeButton: number | null = null; @Output() openDrawer: EventEmitter = new EventEmitter(); @@ -41,8 +46,16 @@ export class GraphToolBarComponent { this.activeButton = index; } - openMenu(){ + openMenu() { this.setActiveButton(4); this.openDrawer.emit(true); } + + toggleRenderEdgeLabel() { + this.sigmaService.toggleRenderEdgeLabel(); + } + + toggleNodeHover(){ + this.sigmaService.toggleNodeHover(); + } } diff --git a/src/app/models/graph-category.ts b/src/app/models/graph-category.ts index 3655768..a912ce0 100644 --- a/src/app/models/graph-category.ts +++ b/src/app/models/graph-category.ts @@ -1,5 +1,5 @@ export interface graphCategory { - sourceCategoryName: string; - targetCategoryName: string; - edgeCategoryName: string; + SourceNodeCategoryName: string; + TargetNodeCategoryName: string; + EdgeCategoryName: string; } diff --git a/src/app/models/graph-records.ts b/src/app/models/graph-records.ts index cb3b57a..3e52b55 100644 --- a/src/app/models/graph-records.ts +++ b/src/app/models/graph-records.ts @@ -1,4 +1,5 @@ export interface graphRecords { nodes: { id: string; label: string }[]; edges: { id: string; source: string; target: string }[]; + message?: string; } diff --git a/src/app/models/search-graph-node.ts b/src/app/models/search-graph-node.ts new file mode 100644 index 0000000..2d130cc --- /dev/null +++ b/src/app/models/search-graph-node.ts @@ -0,0 +1,8 @@ +export interface searchGraphNode { + sourceCategoryName: string; + targetCategoryName: string; + edgeCategoryName: string; + sourceCategoryClauses: object; + targetCategoryClauses: object; + edgeCategoryClauses: object; +} diff --git a/src/app/services/graph/graph.service.spec.ts b/src/app/services/graph/graph.service.spec.ts index 097750f..ffbe9a6 100644 --- a/src/app/services/graph/graph.service.spec.ts +++ b/src/app/services/graph/graph.service.spec.ts @@ -203,9 +203,9 @@ describe('GraphService', () => { // Arrange const id = 1; const categories: graphCategory = { - sourceCategoryName: 'Category1', - targetCategoryName: 'Category2', - edgeCategoryName: 'EdgeCategory1', + SourceNodeCategoryName: 'Category1', + TargetNodeCategoryName: 'Category2', + EdgeCategoryName: 'EdgeCategory1', }; const mockResponse: graphRecords = { nodes: [], edges: [] }; httpClientSpy.get.and.returnValue(of(mockResponse)); @@ -219,7 +219,7 @@ describe('GraphService', () => { expect(httpClientSpy.get.calls.count()).toBe(1); expect(httpClientSpy.get.calls.mostRecent().args[0]).toBe( - `${API_URL}/api/Graph/expansion?nodeId=${id}&sourceCategoryName=${categories.sourceCategoryName}&targetCategoryName=${categories.targetCategoryName}&edgeCategoryName=${categories.edgeCategoryName}` + `${API_URL}/api/Graph/expansion?nodeId=${id}&sourceCategoryName=${categories.SourceNodeCategoryName}&targetCategoryName=${categories.TargetNodeCategoryName}&edgeCategoryName=${categories.EdgeCategoryName}` ); }); }); diff --git a/src/app/services/graph/graph.service.ts b/src/app/services/graph/graph.service.ts index d71bebd..140620f 100644 --- a/src/app/services/graph/graph.service.ts +++ b/src/app/services/graph/graph.service.ts @@ -6,6 +6,7 @@ import { nodeData } from '../../models/node-data'; import { edgeData } from '../../models/edge-data'; import { graphRecords } from '../../models/graph-records'; import { graphCategory } from '../../models/graph-category'; +import { searchGraphNode } from '../../models/search-graph-node'; @Injectable({ providedIn: 'root', @@ -73,8 +74,14 @@ export class GraphService { const headers = { 'Content-Type': 'application/json' }; return this.http.get( - `${this.URL}/api/Graph/expansion?nodeId=${id}&sourceCategoryName=${categories.sourceCategoryName}&targetCategoryName=${categories.targetCategoryName}&edgeCategoryName=${categories.edgeCategoryName}`, + `${this.URL}/api/Graph/expansion?nodeId=${id}&sourceCategoryName=${categories.SourceNodeCategoryName}&targetCategoryName=${categories.TargetNodeCategoryName}&edgeCategoryName=${categories.EdgeCategoryName}`, { headers, withCredentials: true } ); } + + searchNode(data: searchGraphNode) { + const headers = { 'Content-Type': 'application/json' }; + + return this.http.post(`${this.URL}/api/Graph`, data, { headers, withCredentials: true }); + } } diff --git a/src/app/services/sigma/sigma.service.ts b/src/app/services/sigma/sigma.service.ts index a0d0825..f1be705 100644 --- a/src/app/services/sigma/sigma.service.ts +++ b/src/app/services/sigma/sigma.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@angular/core'; import { BehaviorSubject, Subject } from 'rxjs'; import { GraphData } from '../../models/graph-data'; import { graphCategory } from '../../models/graph-category'; +import { graphRecords } from '../../models/graph-records'; @Injectable({ providedIn: 'root', @@ -50,12 +51,27 @@ export class SigmaService { getGraph$ = this.getGraph.asObservable(); private selectedGraphCategories = new BehaviorSubject( { - sourceCategoryName: '', - targetCategoryName: '', - edgeCategoryName: '', + SourceNodeCategoryName: '', + TargetNodeCategoryName: '', + EdgeCategoryName: '', }); selectedGraphCategories$ = this.selectedGraphCategories.asObservable(); + private sourceNodeProp = new BehaviorSubject(['']); + sourceNodeProp$ = this.sourceNodeProp.asObservable(); + + private targetNodeProp = new BehaviorSubject(['']); + targetNodeProp$ = this.targetNodeProp.asObservable(); + + private edgeProp = new BehaviorSubject(['']); + edgeProp$ = this.edgeProp.asObservable(); + + private renderEdgeLabel = new BehaviorSubject(true); + renderEdgeLabel$ = this.renderEdgeLabel.asObservable(); + + private toggleHover = new BehaviorSubject(false); + toggleHover$ = this.toggleHover.asObservable(); + changeData(data: GraphData) { this.graphData.next(data); } @@ -89,12 +105,31 @@ export class SigmaService { this.searchedNode.next(node); } - setGetGraph(data: {nodes: {id:string , label:string}[] , edges: {id:string , source:string , target: string}[]}) { + setGetGraph(data: graphRecords) { this.getGraph.next(data); } setSelectedCategories(data:graphCategory){ this.selectedGraphCategories.next(data); - + } + + setSourceNodeProperties(data: string[]) { + this.sourceNodeProp.next(data) + } + + setTargetNodeProperties(data: string[]) { + this.targetNodeProp.next(data) + } + + setEdgeProperties(data: string[]) { + this.edgeProp.next(data) + } + + toggleRenderEdgeLabel(){ + this.renderEdgeLabel.next(!this.renderEdgeLabel.value); + } + + toggleNodeHover(){ + this.toggleHover.next(!this.toggleHover.value); } } \ No newline at end of file