Skip to content

Commit 5b3db88

Browse files
authored
Merge pull request #115 from delphi-hub/feature/dragAndDropEdges
Feature/drag and drop edges
2 parents 628f83e + 51e94cc commit 5b3db88

16 files changed

+406
-67
lines changed

app/controllers/ApiRouter.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,6 @@ class ApiRouter @Inject()(irController: InstanceRegistryController, sysControlle
4343
case POST(p"/pauseInstance" ? q"instanceID=$instanceID") => irController.handleRequest(action="/pause", instanceID)
4444
case POST(p"/resumeInstance" ? q"instanceID=$instanceID") => irController.handleRequest(action="/resume", instanceID)
4545
case POST(p"/deleteInstance" ? q"instanceID=$instanceID") => irController.handleRequest(action="/delete", instanceID)
46-
46+
case POST(p"/reconnectInstance" ? q"from=$from"& q"to=$to") => irController.reconnect(from.toInt, to.toInt)
4747
}
4848
}

app/controllers/InstanceRegistryController.scala

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,21 @@ class InstanceRegistryController @Inject()(implicit system: ActorSystem, mat: Ma
146146
}(myExecutionContext)
147147
}
148148

149+
def reconnect(from: Int, to: Int): Action[AnyContent] = Action.async { request =>
150+
151+
ws.url(instanceRegistryUri + "/instances/" + from + "/assignInstance"
152+
)
153+
.withHttpHeaders(("Authorization", s"Bearer ${AuthProvider.generateJwt()}"))
154+
.post(Json.obj("AssignedInstanceId" -> to))
155+
.map { response =>
156+
response.status match {
157+
case 200 =>
158+
Ok(response.body)
159+
case x =>
160+
new Status(x)
161+
}
162+
}(myExecutionContext)
163+
}
149164
/**
150165
* This function is for handling an POST request for adding an instance to the Scala web server
151166
* (E.g. .../instances/deploy

client/package-lock.json

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"bootstrap": "^4.3.1",
2626
"core-js": "^2.6.4",
2727
"cytoscape": "^3.4.1",
28+
"cytoscape-edgehandles": "^3.5.0",
2829
"font-awesome": "^4.7.0",
2930
"hammerjs": "^2.0.8",
3031
"jquery": "^3.3.1",

client/src/app/api/api/api.service.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ import {
3535
PAUSE_INSTANCE,
3636
RESUME_INSTANCE,
3737
DELETE_INSTANCE,
38-
INSTANCE_NETWORK
38+
INSTANCE_NETWORK,
39+
RECONNECT
3940
} from '../variables';
4041

4142

@@ -69,6 +70,19 @@ export class ApiService {
6970
return this.get<Array<Instance>>(INSTANCE_NETWORK);
7071
}
7172

73+
public postReconnect(from: number, to: number) {
74+
75+
let queryParam = new HttpParams({ encoder: new CustomHttpUrlEncodingCodec() });
76+
77+
if (from === null || to === undefined) {
78+
throw new Error('Parameter to or from not given');
79+
} else {
80+
queryParam = queryParam.set('from', <any>from);
81+
queryParam = queryParam.set('to', <any>to);
82+
}
83+
84+
return this.commonConf(RECONNECT, queryParam);
85+
}
7286
/**
7387
* Find number of running instances
7488
* How many instances per type are running

client/src/app/api/variables.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export const STOP_INSTANCE = 'api/stopInstance';
2929
export const PAUSE_INSTANCE = 'api/pauseInstance';
3030
export const RESUME_INSTANCE = 'api/resumeInstance';
3131
export const DELETE_INSTANCE = 'api/deleteInstance';
32+
export const RECONNECT = 'api/reconnectInstance';
3233
export const COLLECTION_FORMATS = {
3334
'csv': ',',
3435
'tsv': ' ',
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
export class GraphConfig {
2+
3+
4+
public readonly layout = {
5+
name: 'circle',
6+
7+
fit: true, // whether to fit the viewport to the graph
8+
padding: 30, // the padding on fit
9+
boundingBox: undefined, // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
10+
avoidOverlap: true, // prevents node overlap, may overflow boundingBox and radius if not enough space
11+
nodeDimensionsIncludeLabels: false, // Excludes the label when calculating node bounding boxes for the layout algorithm
12+
spacingFactor: undefined, // Applies a multiplicative factor (>0) to expand or compress the overall area that the nodes take up
13+
radius: undefined, // the radius of the circle
14+
startAngle: 3 / 2 * Math.PI, // where nodes start in radians
15+
sweep: undefined, // how many radians should be between the first and last node (defaults to full circle)
16+
clockwise: true, // whether the layout should go clockwise (true) or counterclockwise/anticlockwise (false)
17+
sort: undefined, // a sorting function to order the nodes; e.g. function(a, b){ return a.data('weight') - b.data('weight') }
18+
animate: false, // whether to transition the node positions
19+
animationDuration: 500, // duration of animation in ms if enabled
20+
animationEasing: undefined, // easing of animation if enabled
21+
animateFilter: function ( node, i ) { return true; },
22+
ready: undefined, // callback on layoutready
23+
stop: undefined, // callback on layoutstop
24+
transform: function (node, position ) { return position; }
25+
};
26+
27+
public readonly cytoscapeConfig = {
28+
container: null, // container to render in
29+
elements: [ // list of graph elements to start with
30+
],
31+
layout: this.layout,
32+
style: [{
33+
selector: 'node[name][image]',
34+
style: {
35+
label: 'data(name)',
36+
'background-image': 'data(image)',
37+
'width': '70%',
38+
'height': '70%',
39+
'background-opacity': 0,
40+
'background-fit': 'contain',
41+
'background-clip': 'none',
42+
}
43+
}, {
44+
selector: '.eh-handle',
45+
style: {
46+
'background-image': '../../../assets/images/EdgeConnector.png',
47+
'width': '60%',
48+
'height': '60%',
49+
'background-opacity': 0,
50+
'border-width': 12, // makes the handle easier to hit
51+
'background-fit': 'contain',
52+
'background-clip': 'none',
53+
'border-opacity': 0
54+
}
55+
},
56+
{
57+
selector: '.eh-hover',
58+
style: {
59+
'background-color': 'red'
60+
}
61+
}]
62+
};
63+
64+
public readonly edgeDragConfig = {
65+
preview: true, // whether to show added edges preview before releasing selection
66+
hoverDelay: 150, // time spent hovering over a target node before it is considered selected
67+
handleNodes: 'node[type != "ElasticSearch"]', // selector/filter function for whether edges can be made from a given node
68+
snap: false, // when enabled, the edge can be drawn by just moving close to a target node (can be confusing on compound graphs)
69+
snapThreshold: 50, // the target node must be less than or equal to this many pixels away from the cursor/finger
70+
snapFrequency: 15, // the number of times per second (Hz) that snap checks done (lower is less expensive)
71+
noEdgeEventsInDraw: false, // set events:no to edges during draws, prevents mouseouts on compounds
72+
disableBrowserGestures: true,
73+
handlePosition: function( node ) {
74+
return 'middle top'; // sets the position of the handle in the format of "X-AXIS Y-AXIS" such as "left top", "middle top"
75+
},
76+
handleInDrawMode: false, // whether to show the handle in draw mode
77+
edgeType: function( sourceNode, targetNode ) {
78+
// can return 'flat' for flat edges between nodes or 'node' for intermediate node between them
79+
// returning null/undefined means an edge can't be added between the two nodes
80+
return 'flat';
81+
},
82+
loopAllowed: function( node ) {
83+
// for the specified node, return whether edges from itself to itself are allowed
84+
return false;
85+
},
86+
nodeLoopOffset: -50, // offset for edgeType: 'node' loops
87+
nodeParams: function( sourceNode, targetNode ) {
88+
// for edges between the specified source and target
89+
// return element object to be passed to cy.add() for intermediary node
90+
return {};
91+
},
92+
edgeParams: function( sourceNode, targetNode, i ) {
93+
// for edges between the specified source and target
94+
// return element object to be passed to cy.add() for edge
95+
// NB: i indicates edge index in case of edgeType: 'node'
96+
return {};
97+
},
98+
ghostEdgeParams: function() {
99+
// return element object to be passed to cy.add() for the ghost edge
100+
// (default classes are always added for you)
101+
return {};
102+
}
103+
};
104+
105+
constructor() {}
106+
}

client/src/app/dashboard/graph-view/connect-dialog/connect-dialog.component.css

Whitespace-only changes.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<h1 mat-dialog-title>Reconnect Components</h1>
2+
<div mat-dialog-content>
3+
<p>Do you really want to disconnect {{data.nameOne}} from {{data.nameTwo}} and connect it to {{data.nameThree}}?</p>
4+
</div>
5+
<div mat-dialog-actions>
6+
<button mat-button [mat-dialog-close]="false">No Thanks</button>
7+
<button mat-button [mat-dialog-close]="true" cdkFocusInitial>Ok</button>
8+
</div>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
2+
3+
import { ConnectDialogComponent } from './connect-dialog.component';
4+
import { MaterialModule } from 'src/app/material-module/material.module';
5+
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
6+
7+
describe('ConnectDialogComponent', () => {
8+
let component: ConnectDialogComponent;
9+
let fixture: ComponentFixture<ConnectDialogComponent>;
10+
11+
beforeEach(async(() => {
12+
TestBed.configureTestingModule({
13+
declarations: [ ConnectDialogComponent ],
14+
imports: [MaterialModule],
15+
providers: [ {
16+
provide: MAT_DIALOG_DATA,
17+
useValue: {}
18+
}]
19+
})
20+
.compileComponents();
21+
}));
22+
23+
beforeEach(() => {
24+
fixture = TestBed.createComponent(ConnectDialogComponent);
25+
component = fixture.componentInstance;
26+
fixture.detectChanges();
27+
});
28+
29+
it('should create', () => {
30+
expect(component).toBeTruthy();
31+
});
32+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { Component, OnInit, Inject } from '@angular/core';
2+
import { MAT_DIALOG_DATA } from '@angular/material';
3+
4+
@Component({
5+
selector: 'app-connect-dialog',
6+
templateUrl: './connect-dialog.component.html',
7+
styleUrls: ['./connect-dialog.component.css']
8+
})
9+
export class ConnectDialogComponent implements OnInit {
10+
11+
constructor(@Inject(MAT_DIALOG_DATA) public data: {nameOne: string, nameTwo: string, nameThree: string}) { }
12+
13+
ngOnInit() {
14+
}
15+
16+
}

client/src/app/dashboard/graph-view/graph-view.module.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,21 @@ import {CommonModule} from '@angular/common';
33
import {GraphViewComponent} from './graph-view/graph-view.component';
44
import {GraphViewService} from './graph-view.service';
55
import {ModelModule} from '../../model/model.module';
6+
import { ConnectDialogComponent } from './connect-dialog/connect-dialog.component';
7+
import { MaterialModule } from 'src/app/material-module/material.module';
68

79
@NgModule({
8-
declarations: [GraphViewComponent],
10+
declarations: [GraphViewComponent, ConnectDialogComponent],
911
imports: [
1012
CommonModule,
13+
MaterialModule,
1114
ModelModule
1215
],
1316
exports: [GraphViewComponent],
14-
providers: [GraphViewService]
17+
providers: [GraphViewService],
18+
entryComponents: [
19+
ConnectDialogComponent
20+
]
1521
})
1622
export class GraphViewModule {
1723
}

0 commit comments

Comments
 (0)