diff --git a/demo/src/app/app.component.html b/demo/src/app/app.component.html
index 9e402a9..fa1c99a 100644
--- a/demo/src/app/app.component.html
+++ b/demo/src/app/app.component.html
@@ -1,5 +1,5 @@
+
diff --git a/demo/src/app/components/misc-scrollbar-demo-component/misc-scrollbar-horizontal-demo.component.ts b/demo/src/app/components/misc-scrollbar-demo-component/misc-scrollbar-horizontal-demo.component.ts
index 5ef0c59..af39197 100644
--- a/demo/src/app/components/misc-scrollbar-demo-component/misc-scrollbar-horizontal-demo.component.ts
+++ b/demo/src/app/components/misc-scrollbar-demo-component/misc-scrollbar-horizontal-demo.component.ts
@@ -8,7 +8,7 @@ import { DEFAULT_SCROLLBAR_THICKNESS } from '../../../../../src/constants/dimens
@Component({
selector: 'misc-scrollbar-horizontal-demo',
template: `
-
` })
@@ -23,10 +23,8 @@ export class MiscScrollbarHorizontalDemoComponent implements AfterViewInit {
// sets up Paper Project
const proj = this.demo.getProject();
proj.activate();
- proj.activeLayer.applyMatrix = false;
this.demo.backgroundColor = CANVAS_BACKGROUND_COLOR;
const view = paper.view;
- const canvas = this.demo.canvas.nativeElement;
const VIEW_PADDING = 30;
// create content
@@ -37,13 +35,13 @@ export class MiscScrollbarHorizontalDemoComponent implements AfterViewInit {
: i % 15 === 0 && 'fizzbuzz' || i % 3 === 0 && 'fizz' || i % 5 === 0 && 'buzz' || i;
content.addChildren([
new paper.Path.Circle({
- position: new paper.Point((100 + 15) * i + 50, view.center.y - 15),
+ position: new paper.Point((100 + 15) * i + 50, view.center.y),
radius: 50,
strokeWidth: 1,
strokeColor: LIGHT_GREY
}),
new paper.PointText({
- point: new paper.Point((100 + 15) * i + 50, view.center.y + 10 - 15),
+ point: new paper.Point((100 + 15) * i + 50, view.center.y + 10),
content: textContent,
fillColor: LIGHT_GREY,
fontSize: 25,
@@ -54,21 +52,16 @@ export class MiscScrollbarHorizontalDemoComponent implements AfterViewInit {
content.translate(new paper.Point(VIEW_PADDING, 0));
// create scrollbar
- const scrollbar = new ScrollbarComponent(
- { content: content, containerBounds: view.bounds, contentOffsetEnd: VIEW_PADDING },
- new paper.Point(VIEW_PADDING, view.bounds.bottom - VIEW_PADDING - DEFAULT_SCROLLBAR_THICKNESS),
- view.bounds.width - VIEW_PADDING * 2,
- 'horizontal'
+ const scrollbar = new ScrollbarComponent({
+ content: content,
+ container: view.element,
+ containerBounds: view.bounds,
+ contentOffsetEnd: VIEW_PADDING
+ },
+ new paper.Point(VIEW_PADDING, view.size.height - DEFAULT_SCROLLBAR_THICKNESS - 10),
+ view.bounds.width - VIEW_PADDING * 2
);
- // add scroll listening. paper doesn't have a wheel event handler
- canvas.onwheel = (event: WheelEvent) => {
- scrollbar.onScroll(event);
- };
- // paper tools are global, so specific tools need to be activated when a different view is active
- view.onMouseEnter = () => {
- scrollbar.activateDefaultTool();
- };
}
run() {
diff --git a/demo/src/app/components/misc-scrollbar-demo-component/misc-scrollbar-vertical-demo.component.ts b/demo/src/app/components/misc-scrollbar-demo-component/misc-scrollbar-vertical-demo.component.ts
index bd75978..9b681e1 100644
--- a/demo/src/app/components/misc-scrollbar-demo-component/misc-scrollbar-vertical-demo.component.ts
+++ b/demo/src/app/components/misc-scrollbar-demo-component/misc-scrollbar-vertical-demo.component.ts
@@ -1,15 +1,14 @@
import { AfterViewInit, Component, ViewChild } from '@angular/core';
import * as paper from 'paper';
import { DemoComponent } from '../demo-component/demo.component';
-import { LIGHT_GREY, CANVAS_BACKGROUND_COLOR, VAPP_BACKGROUND_COLOR } from '../../../../../src/constants/colors';
+import { LIGHT_GREY, CANVAS_BACKGROUND_COLOR } from '../../../../../src/constants/colors';
import { ScrollbarComponent } from '../../../../../src/components/scrollbar';
-import { DEFAULT_SCROLLBAR_THICKNESS } from '../../../../../src/constants/dimensions';
@Component({
selector: 'misc-scrollbar-vertical-demo',
template: `
` })
export class MiscScrollbarVerticalDemoComponent implements AfterViewInit {
@@ -23,10 +22,8 @@ export class MiscScrollbarVerticalDemoComponent implements AfterViewInit {
// sets up Paper Project
const proj = this.demo.getProject();
proj.activate();
- proj.activeLayer.applyMatrix = false;
this.demo.backgroundColor = CANVAS_BACKGROUND_COLOR;
const view = paper.view;
- const canvas = this.demo.canvas.nativeElement;
const VIEW_PADDING = 30;
// create content
@@ -54,57 +51,17 @@ export class MiscScrollbarVerticalDemoComponent implements AfterViewInit {
content.translate(new paper.Point(0, VIEW_PADDING));
// create scrollbar
- const scrollbar = new ScrollbarComponent(
- { content: content, containerBounds: view.bounds, contentOffsetEnd: VIEW_PADDING },
- new paper.Point(view.bounds.right - VIEW_PADDING - DEFAULT_SCROLLBAR_THICKNESS, VIEW_PADDING),
+ const scrollbar = new ScrollbarComponent({
+ content: content,
+ container: view.element,
+ containerBounds: view.bounds,
+ contentOffsetEnd: VIEW_PADDING
+ },
+ new paper.Point(view.bounds.right - VIEW_PADDING, VIEW_PADDING),
view.bounds.height - VIEW_PADDING * 2,
- 'vertical');
+ 'vertical'
+ );
- // add scroll listening. paper doesn't have a wheel event handler
- canvas.onwheel = (event: WheelEvent) => {
- scrollbar.onScroll(event);
- };
- // paper tools are global, so specific tools need to be activated when a different view is active
- view.onMouseEnter = () => {
- scrollbar.activateDefaultTool();
- };
-
- scrollbar.getScrollbar().fillColor = 'red';
- scrollbar.getTrack().fillColor = 'blue';
-
- // set up custom scrollbar
- const customScrollbar = new paper.Path.Rectangle({
- point: new paper.Point(-6.5, 0),
- size: new paper.Size(15, 15),
- pivot: new paper.Point(0, 0),
- radius: 15 / 2,
- fillColor: LIGHT_GREY
- });
- customScrollbar.remove();
- scrollbar.setScrollbar(customScrollbar);
-
- // set up custom track
- const customTrack = new paper.Path.Rectangle({
- point: new paper.Point(0, 0),
- size: new paper.Size(2, view.bounds.height - VIEW_PADDING * 2),
- fillColor: VAPP_BACKGROUND_COLOR
- });
- customTrack.remove();
- scrollbar.setTrack(customTrack);
-
- // set custom Effects
- (scrollbar.getScrollbar() as paper.Path).opacity = 1;
- scrollbar.disableDefaultEffects();
- scrollbar.setCustomEffects({
- setActive: () => {
- (scrollbar.getScrollbar() as paper.Path).fillColor = 'DeepSkyBlue';
- },
- setNormal: () => {
- (scrollbar.getScrollbar() as any).tweenTo({
- fillColor: LIGHT_GREY
- }, 250);
- }
- });
}
run() {
diff --git a/demo/src/app/components/vapp-page-component/vapp-page.component.ts b/demo/src/app/components/vapp-page-component/vapp-page.component.ts
new file mode 100644
index 0000000..5e42f3b
--- /dev/null
+++ b/demo/src/app/components/vapp-page-component/vapp-page.component.ts
@@ -0,0 +1,12 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'vapp-demo',
+ template: `
+
+
+
+ `
+})
+export class VappPageComponent {
+}
diff --git a/demo/src/app/components/vapp-static-demo-component/vapp-static-demo.component.ts b/demo/src/app/components/vapp-static-demo-component/vapp-static-demo.component.ts
new file mode 100644
index 0000000..583bdd6
--- /dev/null
+++ b/demo/src/app/components/vapp-static-demo-component/vapp-static-demo.component.ts
@@ -0,0 +1,57 @@
+import { AfterViewInit, Component, ViewChild } from '@angular/core';
+import * as paper from 'paper';
+import { DemoComponent } from '../demo-component/demo.component';
+import { VappData, VappComponent } from '../../../../../src/components/vapp';
+import { placeholderArrayOfVappData } from '../../constants/vapp-static-placeholder-data';
+import { ScrollbarComponent } from '../../../../../src/components/scrollbar';
+import { CANVAS_BACKGROUND_COLOR } from '../../../../../src/constants/colors';
+import { CONNECTOR_RADIUS, DEFAULT_SCROLLBAR_THICKNESS } from '../../../../../src/constants/dimensions';
+
+@Component({
+ selector: 'vapp-static-demo',
+ template: `
+
+ ` })
+export class VappStaticDemoComponent implements AfterViewInit {
+
+ @ViewChild(DemoComponent)
+ demo: DemoComponent;
+
+ ngAfterViewInit() {
+ // sets up Paper Project
+ const proj = this.demo.getProject();
+ proj.activate();
+ const view = proj.view;
+ this.demo.backgroundColor = CANVAS_BACKGROUND_COLOR;
+
+ const VIEW_PADDING = 30;
+ const DEMO_VAPP_TOP_ALIGNMENT = 59;
+ const VERTICAL_POSITION = VIEW_PADDING + DEMO_VAPP_TOP_ALIGNMENT + CONNECTOR_RADIUS;
+ const vapps: Array
= placeholderArrayOfVappData;
+
+ const content = new paper.Group();
+
+ // create vapps
+ vapps.forEach(vappData => {
+ const position = new paper.Point(
+ content.lastChild ? content.lastChild.bounds.right : VIEW_PADDING, VERTICAL_POSITION);
+ content.addChild(new VappComponent(vappData, position));
+ });
+ (content.lastChild as VappComponent).margin.right = 0;
+
+ // create view horizontal scrollbar
+ const horizontalScrollbar = new ScrollbarComponent({
+ content: content,
+ container: view.element,
+ containerBounds: view.bounds,
+ contentOffsetEnd: VIEW_PADDING
+ },
+ new paper.Point(VIEW_PADDING, view.size.height - DEFAULT_SCROLLBAR_THICKNESS - 10),
+ view.bounds.width - VIEW_PADDING * 2,
+ 'horizontal'
+ );
+ ScrollbarComponent.defaultScrollbar = horizontalScrollbar;
+
+ }
+}
diff --git a/demo/src/app/components/vm-basic-demo-component/vm-basic-demo.component.ts b/demo/src/app/components/vm-basic-demo-component/vm-basic-demo.component.ts
index 17d7669..c909c9b 100644
--- a/demo/src/app/components/vm-basic-demo-component/vm-basic-demo.component.ts
+++ b/demo/src/app/components/vm-basic-demo-component/vm-basic-demo.component.ts
@@ -21,69 +21,91 @@ export class VmBasicDemoComponent implements AfterViewInit {
proj1.activate();
// tslint:disable-next-line
new VmComponent({
- name: 'ubuntu',
uuid: '',
- operatingSystem: 'ubuntu64Guest'
+ name: 'ubuntu',
+ vapp_uuid: '',
+ operatingSystem: 'ubuntu64Guest',
+ vnics: []
}, new paper.Point(15, 15), true);
// tslint:disable-next-line
new VmComponent({
- name: 'fedora',
uuid: '',
- operatingSystem: 'fedora64Guest'
+ name: 'fedora',
+ vapp_uuid: '',
+ operatingSystem: 'fedora64Guest',
+ vnics: []
}, new paper.Point(15, 55), true);
// tslint:disable-next-line
new VmComponent({
- name: 'windows',
uuid: '',
- operatingSystem: 'windows7Guest'
+ name: 'windows',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: []
}, new paper.Point(15, 95), true);
// tslint:disable-next-line
new VmComponent({
- name: 'windows xp',
uuid: '',
- operatingSystem: 'winXPHomeGuest'
+ name: 'windows xp',
+ vapp_uuid: '',
+ operatingSystem: 'winXPHomeGuest',
+ vnics: []
}, new paper.Point(15, 135), true);
// tslint:disable-next-line
new VmComponent({
- name: 'debian',
uuid: '',
- operatingSystem: 'debian8Guest'
+ name: 'debian',
+ vapp_uuid: '',
+ operatingSystem: 'debian8Guest',
+ vnics: []
}, new paper.Point(15, 175), true);
// tslint:disable-next-line
new VmComponent({
- name: 'redhat',
uuid: '',
- operatingSystem: 'redhatGuest'
+ name: 'redhat',
+ vapp_uuid: '',
+ operatingSystem: 'redhatGuest',
+ vnics: []
}, new paper.Point(15, 215), true);
// tslint:disable-next-line
new VmComponent({
- name: 'generic linux',
uuid: '',
- operatingSystem: 'other24xLinux64Guest'
+ name: 'generic linux',
+ vapp_uuid: '',
+ operatingSystem: 'other24xLinux64Guest',
+ vnics: []
}, new paper.Point(15, 255), true);
// tslint:disable-next-line
new VmComponent({
- name: 'centos',
uuid: '',
- operatingSystem: 'centos64Guest'
+ name: 'centos',
+ vapp_uuid: '',
+ operatingSystem: 'centos64Guest',
+ vnics: []
}, new paper.Point(15, 295), true);
// tslint:disable-next-line
new VmComponent({
- name: 'free bsd',
uuid: '',
- operatingSystem: 'freebsd64Guest'
+ name: 'free bsd',
+ vapp_uuid: '',
+ operatingSystem: 'freebsd64Guest',
+ vnics: []
}, new paper.Point(15, 335), true);
// tslint:disable-next-line
new VmComponent({
- name: 'core os',
uuid: '',
- operatingSystem: 'coreos64Guest'
+ name: 'core os',
+ vapp_uuid: '',
+ operatingSystem: 'coreos64Guest',
+ vnics: []
}, new paper.Point(15, 375), true);
// tslint:disable-next-line
new VmComponent({
- name: 'generic operating system with long name',
uuid: '',
- operatingSystem: 'other' as OperatingSystem
+ name: 'generic operating system with long name',
+ vapp_uuid: '',
+ operatingSystem: 'other' as OperatingSystem,
+ vnics: []
}, new paper.Point(15, 415), true);
}
diff --git a/demo/src/app/components/vm-create-demo-component/vm-create-demo.component.ts b/demo/src/app/components/vm-create-demo-component/vm-create-demo.component.ts
index 6eb9a1a..2aaf684 100644
--- a/demo/src/app/components/vm-create-demo-component/vm-create-demo.component.ts
+++ b/demo/src/app/components/vm-create-demo-component/vm-create-demo.component.ts
@@ -23,19 +23,25 @@ export class VmCreateDemoComponent implements AfterViewInit {
const proj = this.demo.getProject();
proj.activate();
this.vmOne = new VmComponent({
- name: 'fedora',
uuid: '',
- operatingSystem: 'fedora64Guest'
+ name: 'fedora',
+ vapp_uuid: '',
+ operatingSystem: 'redhatGuest',
+ vnics: []
}, new paper.Point(15, 15));
this.vmTwo = new VmComponent({
- name: 'redhat linux vm',
uuid: '',
- operatingSystem: 'redhatGuest'
+ name: 'redhat linux vm',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: []
}, new paper.Point(15, 55));
this.vmThree = new VmComponent({
- name: 'centos vm with a really long name',
uuid: '',
- operatingSystem: 'centos64Guest'
+ name: 'centos vm with a really long name',
+ vapp_uuid: '',
+ operatingSystem: 'centos64Guest',
+ vnics: []
}, new paper.Point(15, 95));
}
diff --git a/demo/src/app/components/vm-delete-demo-component/vm-delete-demo.component.ts b/demo/src/app/components/vm-delete-demo-component/vm-delete-demo.component.ts
index 589904e..4ccf66c 100644
--- a/demo/src/app/components/vm-delete-demo-component/vm-delete-demo.component.ts
+++ b/demo/src/app/components/vm-delete-demo-component/vm-delete-demo.component.ts
@@ -23,19 +23,25 @@ export class VmDeleteDemoComponent implements AfterViewInit {
const proj = this.demo.getProject();
proj.activate();
this.vmOne = new VmComponent({
- name: 'fedora',
uuid: '',
- operatingSystem: 'fedora64Guest'
+ name: 'fedora',
+ vapp_uuid: '',
+ operatingSystem: 'fedora64Guest',
+ vnics: []
}, new paper.Point(15, 15), true);
this.vmTwo = new VmComponent({
- name: 'redhat linux vm',
uuid: '',
- operatingSystem: 'redhatGuest'
+ name: 'redhat linux vm',
+ vapp_uuid: '',
+ operatingSystem: 'redhatGuest',
+ vnics: []
}, new paper.Point(15, 55), true);
this.vmThree = new VmComponent({
- name: 'centos vm with a really long name',
uuid: '',
- operatingSystem: 'centos64Guest'
+ name: 'centos vm with a really long name',
+ vapp_uuid: '',
+ operatingSystem: 'centos64Guest',
+ vnics: []
}, new paper.Point(15, 95), true);
}
diff --git a/demo/src/app/constants/vapp-static-placeholder-data.ts b/demo/src/app/constants/vapp-static-placeholder-data.ts
new file mode 100644
index 0000000..72f8375
--- /dev/null
+++ b/demo/src/app/constants/vapp-static-placeholder-data.ts
@@ -0,0 +1,2145 @@
+import { VappData } from '../../../../src/components/vapp';
+import { OperatingSystem } from 'iland-sdk';
+
+/**
+ * Placeholder vApp data for the Vapp Static Demo
+ */
+
+ // 0. nat-routed vapp network
+ // 1. isolated vapp network
+ // 2. multiple isolated vapp networks
+ // 3. long label name
+ // 4. no vapp network
+ // 5. vapp network with no attached vms or vnics
+ // 6. vm with multiple unattached vnics
+ // 7. max amount of vnics
+ // 8. no vapp network or vnics
+ // 9. attached vnic that is disconnected
+ // 10. multiple nat-routed vapp networks
+ // 11. long vms list with scrollbar
+ // 12. long vms list and nat-routed vapp network with scrollbar
+ // 13. multiple vms with max amount of vnics - width edge case
+ // 14. many vms attached to their own network - width edge case
+ // 15. variations for vnics on multiple vapp networks
+ // 16. variations for unattached vnics
+ // 17. nat-routed vApp network with no attached vms and vnics
+ // 18. vapp with no vapp networks or vms
+ // 19. network-less vm in a list that has other vapp networks
+export const placeholderArrayOfVappData: Array = [
+ // 0. nat-routed vapp network
+ {
+ uuid: '',
+ name: 'Coke RES & BURST',
+ vapp_networks: [
+ {
+ uuid: '0',
+ name: '172.16.55.0 Failover Network',
+ vapp_uuid: '',
+ fence_mode: 'NAT_ROUTED'
+ }
+ ],
+ vms: [
+ {
+ uuid: '',
+ name: 'lin-hytrust-01',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '172.16.55.0 Failover Network',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'Alert Resource Non Regression VM',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 1,
+ network_name: '172.16.55.0 Failover Network',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'AutomatedSecurityTest1',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 2,
+ network_name: '172.16.55.0 Failover Network',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'CatalogResourceNonRegression1',
+ vapp_uuid: '',
+ operatingSystem: 'ubuntu64Guest',
+ vnics: [
+ {
+ vnic_id: 3,
+ network_name: '172.16.55.0 Failover Network',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'lin-hytrust-01',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 4,
+ network_name: '172.16.55.0 Failover Network',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'windows-as-01',
+ vapp_uuid: '',
+ operatingSystem: 'ubuntu64Guest',
+ vnics: [
+ {
+ vnic_id: 5,
+ network_name: '172.16.55.0 Failover Network',
+ is_connected: true
+ }
+ ]
+ }
+ ]
+ },
+ // 1. isolated vapp network
+ {
+ uuid: '',
+ name: 'BC Test vApp',
+ vapp_networks: [
+ {
+ uuid: '0',
+ name: 'JTRAN 172.16.100.0/24',
+ vapp_uuid: '',
+ fence_mode: 'ISOLATED'
+ }
+ ],
+ vms: [
+ {
+ uuid: '',
+ name: 'AutomatedSecurityTest1',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'JTRAN 172.16.100.0/24',
+ is_connected: true
+ }
+ ]
+ }
+ ]
+ },
+ // 2. multiple isolated vapp networks
+ {
+ uuid: '',
+ name: 'BC Test vApp',
+ vapp_networks: [
+ {
+ uuid: '0',
+ name: 'JTRAN 172.16.100.0/24',
+ vapp_uuid: '',
+ fence_mode: 'ISOLATED'
+ },
+ {
+ uuid: '1',
+ name: 'JTRAN 172.16.100.0/24 2',
+ vapp_uuid: '',
+ fence_mode: 'ISOLATED'
+ }
+ ],
+ vms: [
+ {
+ uuid: '',
+ name: 'AutomatedSecurityTest1',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'JTRAN 172.16.100.0/24',
+ is_connected: true
+ },
+ {
+ vnic_id: 0,
+ network_name: 'JTRAN 172.16.100.0/24 2',
+ is_connected: true
+ }
+ ]
+ }
+ ]
+ },
+ // 3. long label name
+ {
+ uuid: '',
+ name: 'BillingResourceNonRegressionoooo',
+ vapp_networks: [
+ {
+ uuid: '0',
+ name: 'A',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '1',
+ name: 'B',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ }
+ ],
+ vms: [
+ {
+ uuid: '',
+ name: 'windows-as-01',
+ vapp_uuid: '',
+ operatingSystem: 'ubuntu64Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: true
+ },
+ {
+ vnic_id: 1,
+ network_name: 'B',
+ is_connected: true
+ }
+ ]
+ }
+ ]
+ },
+ // 4. no vapp network
+ {
+ uuid: '',
+ name: 'Delete me build',
+ vapp_networks: [],
+ vms: [
+ {
+ uuid: '',
+ name: 'Delete me VMs Lin',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '',
+ is_connected: false
+ }
+ ]
+ }
+ ]
+ },
+ // 5. vapp network with no attached vms or vnics
+ {
+ uuid: '',
+ name: 'Coke RES & BURST',
+ vapp_networks: [
+ {
+ uuid: '0',
+ name: 'A',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ }
+ ],
+ vms: []
+ },
+ // 6. vm with multiple unattached vnics
+ {
+ uuid: '',
+ name: 'BC Test vApp',
+ vapp_networks: [
+ {
+ uuid: '0',
+ name: 'A',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ }
+ ],
+ vms: [
+ {
+ uuid: '',
+ name: 'AutomatedSecurityTest1',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '',
+ is_connected: false
+ },
+ {
+ vnic_id: 1,
+ network_name: 'A',
+ is_connected: true
+ },
+ {
+ vnic_id: 2,
+ network_name: '',
+ is_connected: false
+ }
+ ]
+ }
+ ]
+ },
+ // 7. max amount of vnics
+ {
+ uuid: '',
+ name: 'BC Test vApp',
+ vapp_networks: [
+ {
+ uuid: '0',
+ name: 'A',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ }
+ ],
+ vms: [
+ {
+ uuid: '',
+ name: 'AutomatedSecurityTest1',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: true
+ },
+ {
+ vnic_id: 1,
+ network_name: '',
+ is_connected: false
+ },
+ {
+ vnic_id: 2,
+ network_name: '',
+ is_connected: false
+ },
+ {
+ vnic_id: 3,
+ network_name: '',
+ is_connected: false
+ },
+ {
+ vnic_id: 4,
+ network_name: '',
+ is_connected: false
+ },
+ {
+ vnic_id: 5,
+ network_name: '',
+ is_connected: false
+ },
+ {
+ vnic_id: 6,
+ network_name: '',
+ is_connected: false
+ },
+ {
+ vnic_id: 7,
+ network_name: '',
+ is_connected: false
+ },
+ {
+ vnic_id: 8,
+ network_name: '',
+ is_connected: false
+ },
+ {
+ vnic_id: 9,
+ network_name: '',
+ is_connected: false
+ }
+ ]
+ }
+ ]
+ },
+ // 8. no vapp network or vnics
+ {
+ uuid: '',
+ name: 'BC Test vApp',
+ vapp_networks: [],
+ vms: [
+ {
+ uuid: '0',
+ name: 'AutomatedSecurityTest1',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: []
+ }
+ ]
+ },
+ // 9. attached vnic that is disconnected
+ {
+ uuid: '',
+ name: 'BC Test vApp',
+ vapp_networks: [
+ {
+ uuid: '0',
+ name: 'A',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ }
+ ],
+ vms: [
+ {
+ uuid: '',
+ name: 'AutomatedSecurityTest1',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: false
+ }
+ ]
+ }
+ ]
+ },
+ // 10. multiple nat-routed vapp networks
+ {
+ uuid: '',
+ name: 'Coke RES & BURST',
+ vapp_networks: [
+ {
+ uuid: '0',
+ name: '172.16.55.0 Failover Network 1',
+ vapp_uuid: '',
+ fence_mode: 'NAT_ROUTED'
+ },
+ {
+ uuid: '1',
+ name: '172.16.55.0 Failover Network 2',
+ vapp_uuid: '',
+ fence_mode: 'NAT_ROUTED'
+ },
+ {
+ uuid: '2',
+ name: '172.16.55.0 Failover Network 3',
+ vapp_uuid: '',
+ fence_mode: 'NAT_ROUTED'
+ }
+ ],
+ vms: [
+ {
+ uuid: '',
+ name: 'lin-hytrust-01',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '172.16.55.0 Failover Network 1',
+ is_connected: true
+ },
+ {
+ vnic_id: 1,
+ network_name: '172.16.55.0 Failover Network 2',
+ is_connected: true
+ },
+ {
+ vnic_id: 2,
+ network_name: '172.16.55.0 Failover Network 3',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'lin-hytrust-01',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '172.16.55.0 Failover Network 1',
+ is_connected: true
+ },
+ {
+ vnic_id: 1,
+ network_name: '172.16.55.0 Failover Network 2',
+ is_connected: true
+ },
+ {
+ vnic_id: 2,
+ network_name: '172.16.55.0 Failover Network 3',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'lin-hytrust-01',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '172.16.55.0 Failover Network 1',
+ is_connected: true
+ },
+ {
+ vnic_id: 1,
+ network_name: '172.16.55.0 Failover Network 2',
+ is_connected: true
+ },
+ {
+ vnic_id: 2,
+ network_name: '172.16.55.0 Failover Network 3',
+ is_connected: true
+ }
+ ]
+ }
+ ]
+ },
+ // 11. long vms list with scrollbar
+ {
+ uuid: '',
+ name: 'Coke RES & BURST',
+ vapp_networks: [
+ {
+ uuid: '0',
+ name: 'A',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ }
+ ],
+ vms: [
+ {
+ uuid: '',
+ name: 'Alert Resource Non Regression VM',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'AutomatedSecurityTest1',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'CatalogResourceNonRegression1',
+ vapp_uuid: '',
+ operatingSystem: 'ubuntu64Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'lin-hytrust-01',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'Alert Resource Non Regression VM',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'AutomatedSecurityTest1',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'CatalogResourceNonRegression1',
+ vapp_uuid: '',
+ operatingSystem: 'ubuntu64Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'lin-hytrust-01',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'windows-as-01',
+ vapp_uuid: '',
+ operatingSystem: 'ubuntu64Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'Alert Resource Non Regression VM',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'AutomatedSecurityTest1',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'CatalogResourceNonRegression1',
+ vapp_uuid: '',
+ operatingSystem: 'ubuntu64Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'lin-hytrust-01',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'Alert Resource Non Regression VM',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'AutomatedSecurityTest1',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'CatalogResourceNonRegression1',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'CatalogResourceNonRegression1',
+ vapp_uuid: '',
+ operatingSystem: 'ubuntu64Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'lin-hytrust-01',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'windows-as-01',
+ vapp_uuid: '',
+ operatingSystem: 'ubuntu64Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'Alert Resource Non Regression VM',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'AutomatedSecurityTest1',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'CatalogResourceNonRegression1',
+ vapp_uuid: '',
+ operatingSystem: 'ubuntu64Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'lin-hytrust-01',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'Alert Resource Non Regression VM',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'AutomatedSecurityTest1',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'CatalogResourceNonRegression1',
+ vapp_uuid: '',
+ operatingSystem: 'ubuntu64Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'lin-hytrust-01',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'windows-as-01',
+ vapp_uuid: '',
+ operatingSystem: 'ubuntu64Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: true
+ }
+ ]
+ }
+ ]
+ },
+ // 12. long vms list and nat-routed vapp network with scrollbar
+ {
+ uuid: '',
+ name: 'Coke RES & BURST',
+ vapp_networks: [
+ {
+ uuid: '0',
+ name: '172.16.55.0 Failover Network',
+ vapp_uuid: '',
+ fence_mode: 'NAT_ROUTED'
+ }
+ ],
+ vms: [
+ {
+ uuid: '',
+ name: 'Alert Resource Non Regression VM',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '172.16.55.0 Failover Network',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'AutomatedSecurityTest1',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '172.16.55.0 Failover Network',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'CatalogResourceNonRegression1',
+ vapp_uuid: '',
+ operatingSystem: 'ubuntu64Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '172.16.55.0 Failover Network',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'lin-hytrust-01',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '172.16.55.0 Failover Network',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'Alert Resource Non Regression VM',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '172.16.55.0 Failover Network',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'AutomatedSecurityTest1',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '172.16.55.0 Failover Network',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'CatalogResourceNonRegression1',
+ vapp_uuid: '',
+ operatingSystem: 'ubuntu64Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '172.16.55.0 Failover Network',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'lin-hytrust-01',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '172.16.55.0 Failover Network',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'windows-as-01',
+ vapp_uuid: '',
+ operatingSystem: 'ubuntu64Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '172.16.55.0 Failover Network',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'Alert Resource Non Regression VM',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '172.16.55.0 Failover Network',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'AutomatedSecurityTest1',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '172.16.55.0 Failover Network',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'CatalogResourceNonRegression1',
+ vapp_uuid: '',
+ operatingSystem: 'ubuntu64Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '172.16.55.0 Failover Network',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'lin-hytrust-01',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '172.16.55.0 Failover Network',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'Alert Resource Non Regression VM',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '172.16.55.0 Failover Network',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'AutomatedSecurityTest1',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '172.16.55.0 Failover Network',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'CatalogResourceNonRegression1',
+ vapp_uuid: '',
+ operatingSystem: 'ubuntu64Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '172.16.55.0 Failover Network',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'lin-hytrust-01',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '172.16.55.0 Failover Network',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'windows-as-01',
+ vapp_uuid: '',
+ operatingSystem: 'ubuntu64Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '172.16.55.0 Failover Network',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'Alert Resource Non Regression VM',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '172.16.55.0 Failover Network',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'AutomatedSecurityTest1',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '172.16.55.0 Failover Network',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'CatalogResourceNonRegression1',
+ vapp_uuid: '',
+ operatingSystem: 'ubuntu64Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '172.16.55.0 Failover Network',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'lin-hytrust-01',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '172.16.55.0 Failover Network',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'Alert Resource Non Regression VM',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '172.16.55.0 Failover Network',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'AutomatedSecurityTest1',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '172.16.55.0 Failover Network',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'CatalogResourceNonRegression1',
+ vapp_uuid: '',
+ operatingSystem: 'ubuntu64Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '172.16.55.0 Failover Network',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'lin-hytrust-01',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '172.16.55.0 Failover Network',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'windows-as-01',
+ vapp_uuid: '',
+ operatingSystem: 'ubuntu64Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '172.16.55.0 Failover Network',
+ is_connected: true
+ }
+ ]
+ }
+ ]
+ },
+ // 13. multiple vms with max amount of vnics - width edge case
+ {
+ uuid: '',
+ name: 'BillingResourceNonRegressionoooo',
+ vapp_networks: [
+ {
+ uuid: '0',
+ name: '0',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '1',
+ name: '1',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '2',
+ name: '2',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '3',
+ name: '3',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '4',
+ name: '4',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '5',
+ name: '5',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '6',
+ name: '6',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '7',
+ name: '7',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '8',
+ name: '8',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '9',
+ name: '9',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '10',
+ name: '10',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '11',
+ name: '11',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '12',
+ name: '12',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '13',
+ name: '13',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '14',
+ name: '14',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '15',
+ name: '15',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '16',
+ name: '16',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '17',
+ name: '17',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '18',
+ name: '18',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '19',
+ name: '19',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '20',
+ name: '20',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '21',
+ name: '21',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '22',
+ name: '22',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '23',
+ name: '23',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '24',
+ name: '24',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '25',
+ name: '25',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '26',
+ name: '26',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '27',
+ name: '27',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '28',
+ name: '28',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '29',
+ name: '29',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ }
+ ],
+ vms: [
+ {
+ uuid: '',
+ name: 'windows-as-01',
+ vapp_uuid: '',
+ operatingSystem: 'ubuntu64Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '0',
+ is_connected: true
+ },
+ {
+ vnic_id: 1,
+ network_name: '1',
+ is_connected: true
+ },
+ {
+ vnic_id: 2,
+ network_name: '2',
+ is_connected: true
+ },
+ {
+ vnic_id: 3,
+ network_name: '3',
+ is_connected: true
+ },
+ {
+ vnic_id: 4,
+ network_name: '4',
+ is_connected: true
+ },
+ {
+ vnic_id: 5,
+ network_name: '5',
+ is_connected: true
+ },
+ {
+ vnic_id: 6,
+ network_name: '6',
+ is_connected: true
+ },
+ {
+ vnic_id: 7,
+ network_name: '7',
+ is_connected: true
+ },
+ {
+ vnic_id: 8,
+ network_name: '8',
+ is_connected: true
+ },
+ {
+ vnic_id: 9,
+ network_name: '9',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'AutomatedSecurityTest1',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '10',
+ is_connected: true
+ },
+ {
+ vnic_id: 1,
+ network_name: '11',
+ is_connected: true
+ },
+ {
+ vnic_id: 2,
+ network_name: '12',
+ is_connected: true
+ },
+ {
+ vnic_id: 3,
+ network_name: '13',
+ is_connected: true
+ },
+ {
+ vnic_id: 4,
+ network_name: '14',
+ is_connected: true
+ },
+ {
+ vnic_id: 5,
+ network_name: '15',
+ is_connected: true
+ },
+ {
+ vnic_id: 6,
+ network_name: '16',
+ is_connected: true
+ },
+ {
+ vnic_id: 7,
+ network_name: '17',
+ is_connected: true
+ },
+ {
+ vnic_id: 8,
+ network_name: '18',
+ is_connected: true
+ },
+ {
+ vnic_id: 9,
+ network_name: '19',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'AutomatedSecurityTest2',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '20',
+ is_connected: true
+ },
+ {
+ vnic_id: 1,
+ network_name: '21',
+ is_connected: true
+ },
+ {
+ vnic_id: 2,
+ network_name: '22',
+ is_connected: true
+ },
+ {
+ vnic_id: 3,
+ network_name: '23',
+ is_connected: true
+ },
+ {
+ vnic_id: 4,
+ network_name: '24',
+ is_connected: true
+ },
+ {
+ vnic_id: 5,
+ network_name: '25',
+ is_connected: true
+ },
+ {
+ vnic_id: 6,
+ network_name: '26',
+ is_connected: true
+ },
+ {
+ vnic_id: 7,
+ network_name: '27',
+ is_connected: true
+ },
+ {
+ vnic_id: 8,
+ network_name: '28',
+ is_connected: true
+ },
+ {
+ vnic_id: 9,
+ network_name: '29',
+ is_connected: true
+ }
+ ]
+ }
+ ]
+ },
+ // 14. many vms attached to their own network - width edge case
+ {
+ uuid: '',
+ name: 'BillingResourceNonRegressionoooo',
+ vapp_networks: [
+ {
+ uuid: '0',
+ name: '0',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '1',
+ name: '1',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '2',
+ name: '2',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '3',
+ name: '3',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '4',
+ name: '4',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '5',
+ name: '5',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '6',
+ name: '6',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '7',
+ name: '7',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '8',
+ name: '8',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '9',
+ name: '9',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '10',
+ name: '10',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '11',
+ name: '11',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '12',
+ name: '12',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ }
+ ],
+ vms: [
+ {
+ uuid: '',
+ name: 'Alert Resource Non Regression VM',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '0',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'AutomatedSecurityTest1',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '1',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'CatalogResourceNonRegression1',
+ vapp_uuid: '',
+ operatingSystem: 'ubuntu64Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '2',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'lin-hytrust-01',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '3',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'Alert Resource Non Regression VM',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '4',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'AutomatedSecurityTest1',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '5',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'CatalogResourceNonRegression1',
+ vapp_uuid: '',
+ operatingSystem: 'ubuntu64Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '6',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'lin-hytrust-01',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '7',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'windows-as-01',
+ vapp_uuid: '',
+ operatingSystem: 'ubuntu64Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '8',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'Alert Resource Non Regression VM',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '9',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'AutomatedSecurityTest1',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '10',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'CatalogResourceNonRegression1',
+ vapp_uuid: '',
+ operatingSystem: 'ubuntu64Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '11',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'lin-hytrust-01',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '12',
+ is_connected: true
+ }
+ ]
+ }
+ ]
+ },
+ // 15. variations for vnics on multiple vapp networks
+ {
+ uuid: '',
+ name: 'BC Test vApp',
+ vapp_networks: [
+ {
+ uuid: '0',
+ name: 'A',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '1',
+ name: 'B',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '2',
+ name: 'C',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '3',
+ name: 'D',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ }
+ ],
+ vms: [
+ {
+ uuid: '',
+ name: 'windows-as-01',
+ vapp_uuid: '',
+ operatingSystem: 'ubuntu64Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: true
+ },
+ {
+ vnic_id: 1,
+ network_name: 'B',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'windows-as-02',
+ vapp_uuid: '',
+ operatingSystem: 'ubuntu64Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'C',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'windows-as-03',
+ vapp_uuid: '',
+ operatingSystem: 'ubuntu64Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'B',
+ is_connected: true
+ },
+ {
+ vnic_id: 1,
+ network_name: 'D',
+ is_connected: true
+ }
+ ]
+ }
+ ]
+ },
+ // 16. variations for unattached vnics
+ {
+ uuid: '',
+ name: 'BC Test vApp',
+ vapp_networks: [
+ {
+ uuid: '1',
+ name: 'A',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '2',
+ name: 'B',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '3',
+ name: 'C',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ }
+ ],
+ vms: [
+ {
+ uuid: '',
+ name: 'windows-as-01',
+ vapp_uuid: '',
+ operatingSystem: 'ubuntu64Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: true
+ },
+ {
+ vnic_id: 1,
+ network_name: '',
+ is_connected: false
+ },
+ {
+ vnic_id: 2,
+ network_name: '',
+ is_connected: false
+ },
+ {
+ vnic_id: 3,
+ network_name: '',
+ is_connected: false
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'windows-as-02',
+ vapp_uuid: '',
+ operatingSystem: 'ubuntu64Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'B',
+ is_connected: true
+ },
+ {
+ vnic_id: 1,
+ network_name: '',
+ is_connected: false
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'windows-as-03',
+ vapp_uuid: '',
+ operatingSystem: 'ubuntu64Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'C',
+ is_connected: true
+ }
+ ]
+ }
+ ]
+ },
+ // 17. nat-routed vApp network with no attached vms and vnics
+ {
+ uuid: '',
+ name: 'Coke RES & BURST',
+ vapp_networks: [
+ {
+ uuid: '0',
+ name: '172.16.55.0 Failover Network 1',
+ vapp_uuid: '',
+ fence_mode: 'NAT_ROUTED'
+ }
+ ],
+ vms: []
+ },
+ // 18. vapp with no vapp networks or vms
+ {
+ uuid: '',
+ name: 'Coke RES & BURST',
+ vapp_networks: [],
+ vms: []
+ },
+ // 19. network-less vm in a list that has other vapp networks
+ {
+ uuid: '',
+ name: 'BC Test vApp',
+ vapp_networks: [
+ {
+ uuid: '0',
+ name: 'A',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '1',
+ name: 'B',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '2',
+ name: 'C',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ }
+ ],
+ vms: [
+ {
+ uuid: '',
+ name: 'redhat-as-01',
+ vapp_uuid: '',
+ operatingSystem: 'redhatGuest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'debian-as-02',
+ vapp_uuid: '',
+ operatingSystem: 'debian8Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'B',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'linux-as-03',
+ vapp_uuid: '',
+ operatingSystem: 'other24xLinux64Guest',
+ vnics: [
+ {
+ vnic_id: 1,
+ network_name: 'C',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'arch-as-04',
+ vapp_uuid: '',
+ operatingSystem: 'btwIUseArch' as OperatingSystem,
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: '',
+ is_connected: false
+ }
+ ]
+ }
+ ]
+ }
+];
diff --git a/demo/src/styles.less b/demo/src/styles.less
index 73ea989..5be7bb0 100644
--- a/demo/src/styles.less
+++ b/demo/src/styles.less
@@ -1,4 +1,7 @@
/* You can add global styles to this file, and also import other style files */
+
+@import (css) url('https://fonts.googleapis.com/css?family=Roboto:400,500,700&display=swap');
+
body {
font-family: 'Roboto', sans-serif;
}
diff --git a/src/.DS_Store b/src/.DS_Store
new file mode 100644
index 0000000..1ff6fd9
Binary files /dev/null and b/src/.DS_Store differ
diff --git a/src/components/bullet-point-connection-icon.test.ts b/src/components/bullet-point-connection-icon.test.ts
new file mode 100644
index 0000000..84861bf
--- /dev/null
+++ b/src/components/bullet-point-connection-icon.test.ts
@@ -0,0 +1,26 @@
+import { BulletPointConnectionIconComponent } from './bullet-point-connection-icon';
+import * as paper from 'paper';
+
+describe('bullet point connection icon component', () => {
+
+ beforeAll(() => {
+ const canvasEl = document.createElement('canvas');
+ paper.setup(canvasEl);
+ paper.settings.applyMatrix = false;
+ });
+
+ test('basic properties', () => {
+ const position = new paper.Point(0, 0);
+ const connector = new BulletPointConnectionIconComponent();
+ expect(connector.position.x).toBe(position.x);
+ expect(connector.position.y).toBe(position.y);
+ });
+
+ test('custom position', () => {
+ const position = new paper.Point(42, -10);
+ const connector = new BulletPointConnectionIconComponent(position);
+ expect(connector.position.x).toBe(position.x);
+ expect(connector.position.y).toBe(position.y);
+ });
+
+});
diff --git a/src/components/bullet-point-connection-icon.ts b/src/components/bullet-point-connection-icon.ts
new file mode 100644
index 0000000..1df6616
--- /dev/null
+++ b/src/components/bullet-point-connection-icon.ts
@@ -0,0 +1,36 @@
+import * as paper from 'paper';
+import { LIGHT_GREY } from '../constants/colors';
+import { SMALL_CONNECTOR_SIZE } from '../constants/dimensions';
+
+/**
+ * Bullet Point Connection Icon Visual Component.
+ * The small (7px) grey filled circle icon that represents special types of connections or bullet points for labels.
+ * Used for isolated vApp Network labels or at the end of vApp networks that have no attached VNICs.
+ */
+export class BulletPointConnectionIconComponent extends paper.Group {
+
+ // the small filled circle icon
+ readonly _icon: paper.Path.Circle;
+
+ /**
+ * Creates a new BulletPointConnectionIconComponent instance.
+ *
+ * @param _point the location that the bullet connection icon should be rendered at
+ */
+ constructor(private _point: paper.Point = new paper.Point(0, 0)) {
+ super();
+ this.position = _point;
+ this.pivot = new paper.Point(0, 0);
+
+ this._icon = new paper.Path.Circle({
+ position: new paper.Point(0, 0),
+ radius: SMALL_CONNECTOR_SIZE / 2,
+ fillColor: LIGHT_GREY,
+ parent: this
+ });
+ }
+
+ get icon(): paper.Path.Circle {
+ return this._icon;
+ }
+}
diff --git a/src/components/connection-icon.test.ts b/src/components/connection-icon.test.ts
new file mode 100644
index 0000000..e73ddb5
--- /dev/null
+++ b/src/components/connection-icon.test.ts
@@ -0,0 +1,31 @@
+import { ConnectionIconComponent } from './connection-icon';
+import { VAPP_BACKGROUND_COLOR, CANVAS_BACKGROUND_COLOR } from '../constants/colors';
+import * as paper from 'paper';
+
+describe('connection icon component', () => {
+
+ beforeAll(() => {
+ const canvasEl = document.createElement('canvas');
+ paper.setup(canvasEl);
+ paper.settings.applyMatrix = false;
+ });
+
+ test('basic properties', () => {
+ const position = new paper.Point(0, 0);
+ const defaultColor = new paper.Color(VAPP_BACKGROUND_COLOR);
+ const connector = new ConnectionIconComponent();
+ expect(connector.position.x).toBe(position.x);
+ expect(connector.position.y).toBe(position.y);
+ expect((connector.icon.fillColor as paper.Color).equals(defaultColor)).toBe(true);
+ });
+
+ test('custom position and fill color', () => {
+ const position = new paper.Point(-20, 30);
+ const color = new paper.Color(CANVAS_BACKGROUND_COLOR);
+ const connector = new ConnectionIconComponent(position, color);
+ expect(connector.position.x).toBe(position.x);
+ expect(connector.position.y).toBe(position.y);
+ expect((connector.icon.fillColor as paper.Color).equals(color)).toBe(true);
+ });
+
+});
diff --git a/src/components/connection-icon.ts b/src/components/connection-icon.ts
new file mode 100644
index 0000000..ba8eb68
--- /dev/null
+++ b/src/components/connection-icon.ts
@@ -0,0 +1,42 @@
+import * as paper from 'paper';
+import { VAPP_BACKGROUND_COLOR } from '../constants/colors';
+import { CONNECTOR_RADIUS } from '../constants/dimensions';
+import { DEFAULT_STROKE_STYLE } from '../constants/styles';
+
+/**
+ * Connection Icon Visual Component.
+ * The large (11px) open circle icon with a grey stroke that represents connections. Used for VNICs that are
+ * attached and connected to a vApp Network or for the vApp Network to Org-Vdc network connections.
+ */
+export class ConnectionIconComponent extends paper.Group {
+
+ // the stroked and 'unfilled' circle icon
+ readonly _icon: paper.Path.Circle;
+
+ /**
+ * Creates a new ConnectionIconComponent instance.
+ *
+ * @param _point the location that the icon should be rendered at
+ * @param _fillColor the inner fill color that usually matches the background color of the element it is on top of
+ * to make it seem 'unfilled'
+ */
+ constructor(private _point: paper.Point = new paper.Point(0, 0),
+ private _fillColor: paper.Color | string = VAPP_BACKGROUND_COLOR) {
+ super();
+ this.position = _point;
+ this.pivot = new paper.Point(0, 0);
+
+ this._icon = new paper.Path.Circle(
+ {
+ position: new paper.Point(0, 0),
+ radius: CONNECTOR_RADIUS,
+ style: DEFAULT_STROKE_STYLE,
+ fillColor: this._fillColor,
+ parent: this
+ });
+ }
+
+ get icon(): paper.Path.Circle {
+ return this._icon;
+ }
+}
diff --git a/src/components/entity-label.test.ts b/src/components/entity-label.test.ts
new file mode 100644
index 0000000..eca0487
--- /dev/null
+++ b/src/components/entity-label.test.ts
@@ -0,0 +1,37 @@
+import { EntityLabelComponent } from './entity-label';
+import * as paper from 'paper';
+import { LABEL_HORIZONTAL_PADDING } from '../constants/dimensions';
+
+describe('entity label component', () => {
+
+ beforeAll(() => {
+ const canvasEl = document.createElement('canvas');
+ paper.setup(canvasEl);
+ paper.settings.applyMatrix = false;
+ });
+
+ test('basic properties', () => {
+ const text = 'foobar';
+ const position = new paper.Point(0, 0);
+ const color = new paper.Color('red');
+ const label = new EntityLabelComponent(text, color);
+ expect(label.position.x).toBe(position.x);
+ expect(label.position.y).toBe(position.y);
+ expect(label.icon.fillColor).toBe(color);
+ });
+
+ test('textOptions', () => {
+ const text = 'foobar';
+ const position = new paper.Point(10, 90);
+ const color = new paper.Color('blue');
+ const textOptions = { fontWeight: 'bold' };
+ const label = new EntityLabelComponent(text, color, position, textOptions);
+ expect(label.position.x).toBe(position.x);
+ expect(label.position.y).toBe(position.y);
+ expect(label.icon.fillColor).toBe(color);
+ expect(label.text.fontWeight).toBe(textOptions.fontWeight);
+ expect(label.bounds.right - LABEL_HORIZONTAL_PADDING)
+ .toBe(label.localToGlobal(label.text.bounds.bottomRight).x);
+ });
+
+});
diff --git a/src/components/entity-label.ts b/src/components/entity-label.ts
new file mode 100644
index 0000000..4be438c
--- /dev/null
+++ b/src/components/entity-label.ts
@@ -0,0 +1,52 @@
+import * as paper from 'paper';
+import { LABEL_HORIZONTAL_PADDING } from '../constants/dimensions';
+import { LabelComponent } from './label';
+import { TextOptions } from './label-text';
+
+const ICON_SIZE = 10;
+const ICON_MARGIN = 10;
+
+/**
+ * Entity Label Visual Component.
+ */
+export class EntityLabelComponent extends LabelComponent {
+
+ // the entity icon
+ readonly _icon: paper.Path.Rectangle;
+
+ /**
+ * Creates a new EntityLabelComponent instance.
+ *
+ * @param _text the text to be displayed on the label
+ * @param iconColor the icon color specific to the entity type
+ * @param _point the location that the entity label should be rendered at
+ * @param textOptions the paper.PointText options object to customize the text
+ */
+ constructor(protected _text: string,
+ protected iconColor: paper.Color | string,
+ protected _point: paper.Point = new paper.Point(0, 0),
+ protected textOptions: TextOptions = {}) {
+ super(_text, _point, textOptions);
+ this.pivot = new paper.Point(0, 0);
+
+ this._icon = new paper.Path.Rectangle({
+ rectangle: new paper.Rectangle(0, 0, ICON_SIZE, ICON_SIZE),
+ radius: 2,
+ pivot: new paper.Point(0, 0),
+ position: new paper.Point(ICON_MARGIN, ICON_MARGIN),
+ fillColor: this.iconColor,
+ parent: this
+ });
+
+ // reposition and resize other elements to fit the icon
+ this._label.position.x = this._icon.bounds.right + LABEL_HORIZONTAL_PADDING;
+ this._background.bounds.width += ICON_SIZE + ICON_MARGIN;
+ }
+
+ /**
+ * Gets the icon path item.
+ */
+ get icon(): paper.Path.Rectangle {
+ return this._icon;
+ }
+}
diff --git a/src/components/icon-label-loader.ts b/src/components/icon-label-loader.ts
index 88d78ba..40cbf6d 100644
--- a/src/components/icon-label-loader.ts
+++ b/src/components/icon-label-loader.ts
@@ -1,9 +1,10 @@
import * as paper from 'paper';
import { VM_ICON_SIZE } from '../constants/dimensions';
+import { CANVAS_BACKGROUND_COLOR } from '../constants/colors';
const SLIDEOVER_ANIMATIONS_PER_SECOND = .125;
const SIZE = 30;
-const BACKGROUND_COLOR = '#191C28';
+const BACKGROUND_COLOR = CANVAS_BACKGROUND_COLOR;
const SPINNER_COLOR = '#81A2B6';
const SPINNER_ARC_WIDTH = 3;
const SPINNER_ARC_MARGIN = 9;
@@ -28,7 +29,6 @@ export class IconLabelLoaderComponent extends paper.Group {
symbolPromise: Promise) {
super();
const self = this;
- this.applyMatrix = false;
this.position = _point;
this._background = new paper.Path.Rectangle({
rectangle: new paper.Rectangle(0, 0,
diff --git a/src/components/icon-label.ts b/src/components/icon-label.ts
index 6edf400..87e5348 100644
--- a/src/components/icon-label.ts
+++ b/src/components/icon-label.ts
@@ -24,7 +24,6 @@ export class IconLabelComponent extends LabelComponent {
constructor(protected _text: string, symbolPromise: Promise,
protected _point: paper.Point = new paper.Point(0, 0)) {
super(_text, _point);
- this.applyMatrix = false;
this.pivot = new paper.Point(0, 0);
const self = this;
// tslint:disable-next-line:no-floating-promises
diff --git a/src/components/isolated-network-label.test.ts b/src/components/isolated-network-label.test.ts
new file mode 100644
index 0000000..aefc9c2
--- /dev/null
+++ b/src/components/isolated-network-label.test.ts
@@ -0,0 +1,31 @@
+import { IsolatedNetworkLabelComponent } from './isolated-network-label';
+import { DEFAULT_MAX_LABEL_WIDTH } from '../constants/dimensions';
+import * as paper from 'paper';
+
+describe('isolated network label component', () => {
+
+ beforeAll(() => {
+ const canvasEl = document.createElement('canvas');
+ paper.setup(canvasEl);
+ paper.settings.applyMatrix = false;
+ });
+
+ test('basic properties', () => {
+ const text = 'foobar';
+ const position = new paper.Point(10, 90);
+ const label = new IsolatedNetworkLabelComponent(text, position);
+ expect(label.position.x).toBe(position.x);
+ expect(label.position.y).toBe(position.y);
+ expect(label.label.text.content).toBe(text.toUpperCase());
+ });
+
+ test('maxWidth', () => {
+ const text = 'really really really really really really really really really really long name';
+ const position = new paper.Point(10, 90);
+ const label = new IsolatedNetworkLabelComponent(text, position);
+ expect(label.position.x).toBe(position.x);
+ expect(label.position.y).toBe(position.y);
+ expect(label.label.bounds.width).toBeLessThan(DEFAULT_MAX_LABEL_WIDTH);
+ });
+
+});
diff --git a/src/components/isolated-network-label.ts b/src/components/isolated-network-label.ts
new file mode 100644
index 0000000..a80ef66
--- /dev/null
+++ b/src/components/isolated-network-label.ts
@@ -0,0 +1,52 @@
+import * as paper from 'paper';
+import { LIGHT_GREY } from '../constants/colors';
+import { BulletPointConnectionIconComponent } from './bullet-point-connection-icon';
+import { LabelTextComponent } from './label-text';
+
+const LABEL_PADDING_LEFT = 10;
+
+/**
+ * Isolated Network Label Visual Component.
+ */
+export class IsolatedNetworkLabelComponent extends paper.Group {
+
+ // the label text
+ private _label: LabelTextComponent;
+ // small circle icon at the top of the isolated vApp network path
+ private icon: BulletPointConnectionIconComponent;
+
+ /**
+ * Creates a new IsolatedNetworkLabelComponent instance.
+ *
+ * @param network the network name
+ * @param _point the location that the isolated network label should be rendered at
+ */
+ constructor(private network: string,
+ private _point: paper.Point = new paper.Point(0, 0)) {
+ super();
+ this.pivot = new paper.Point(0, 0);
+ this.position = _point;
+
+ this.icon = new BulletPointConnectionIconComponent();
+
+ this._label = new LabelTextComponent(
+ this.network.toUpperCase(),
+ new paper.Point(0, 0),{
+ fillColor: LIGHT_GREY,
+ fontWeight: 'bold',
+ fontSize: 10
+ });
+
+ // position the label to the right of the icon
+ this._label.bounds.leftCenter = this.icon.bounds.rightCenter.add(new paper.Point(LABEL_PADDING_LEFT, 0));
+
+ this.addChildren([this.icon, this._label]);
+ }
+
+ /**
+ * Gets the label.
+ */
+ get label(): LabelTextComponent {
+ return this._label;
+ }
+}
diff --git a/src/components/label-text.test.ts b/src/components/label-text.test.ts
new file mode 100644
index 0000000..6abd62d
--- /dev/null
+++ b/src/components/label-text.test.ts
@@ -0,0 +1,65 @@
+import { LabelTextComponent } from './label-text';
+import * as paper from 'paper';
+import { DEFAULT_MAX_LABEL_WIDTH } from '../constants/dimensions';
+
+describe('label text component', () => {
+
+ beforeAll(() => {
+ const canvasEl = document.createElement('canvas');
+ paper.setup(canvasEl);
+ paper.settings.applyMatrix = false;
+ });
+
+ test('basic properties', () => {
+ const text = 'foobar';
+ const position = new paper.Point(0, 0);
+ const label = new LabelTextComponent(text);
+ expect(label.position.x).toBe(position.x);
+ expect(label.position.y).toBe(position.y);
+ expect(label.text.content).toBe(text);
+ });
+
+ test('textOptions configuration', () => {
+ const text = 'old';
+ const position = new paper.Point(20, 50);
+ const textOptions = {
+ fontWeight: 'bold',
+ justification: 'right',
+ fillColor: new paper.Color('blue'),
+ fontSize: 30,
+ leading: 35,
+ content: 'new'
+ };
+ const label = new LabelTextComponent(text, position, textOptions);
+ expect(label.position.x).toBe(position.x);
+ expect(label.position.y).toBe(position.y);
+ expect(label.text.content).toBe(textOptions.content);
+ expect(label.text.fontWeight).toBe(textOptions.fontWeight);
+ expect(label.text.justification).toBe(textOptions.justification);
+ expect(label.text.fillColor).toBe(textOptions.fillColor);
+ expect(label.text.fontSize).toBe(textOptions.fontSize);
+ expect(label.text.leading).toBe(textOptions.leading);
+ });
+
+ test('default maxWidth', () => {
+ const text = 'suuuuuuuuuuper duuuuuuuuuuper loooooooooooooooooong name';
+ const position = new paper.Point(56, 3);
+ const label = new LabelTextComponent(text, position, undefined);
+ expect(label.position.x).toBe(position.x);
+ expect(label.position.y).toBe(position.y);
+ expect(label.bounds.width).toBeLessThan(DEFAULT_MAX_LABEL_WIDTH);
+ });
+
+ test('custom maxWidth', () => {
+ const text = 'suuuuuuuuuuper duuuuuuuuuuper loooooooooooooooooong name';
+ const position = new paper.Point(10, 90);
+ const textOptions = { fontFamily: 'serif' };
+ const maxWidth = 100;
+ const label = new LabelTextComponent(text, position, textOptions, maxWidth);
+ expect(label.position.x).toBe(position.x);
+ expect(label.position.y).toBe(position.y);
+ expect(label.text.fontFamily).toBe(textOptions.fontFamily);
+ expect(label.bounds.width).toBeLessThan(maxWidth);
+ });
+
+});
diff --git a/src/components/label-text.ts b/src/components/label-text.ts
new file mode 100644
index 0000000..7df8129
--- /dev/null
+++ b/src/components/label-text.ts
@@ -0,0 +1,89 @@
+import * as paper from 'paper';
+import { DEFAULT_MAX_LABEL_WIDTH } from '../constants/dimensions';
+import { WHITE } from '../constants/colors';
+
+const TEXT_COLOR = WHITE;
+export const FONT_SIZE = 13;
+const LINE_HEIGHT = 15;
+// TODO: custom font loads asynchronously after the canvas renders. this causes any reliant background items to
+// prematurely render at the wrong size. re-enable 'Roboto' custom font when a solution for canvas font loading is
+// implemented. can try redraw on frame method or redraw once font is loaded to the HTML canvas.
+const FONT_FAMILY = 'Roboto';
+
+export type TextOptions = Record;
+
+/**
+ * Label Text Visual Component.
+ */
+export class LabelTextComponent extends paper.Group {
+
+ // the label text
+ protected _label: paper.PointText;
+
+ /**
+ * Creates a new LabelTextComponent instance.
+ *
+ * @param _text the text to be displayed
+ * @param _point the location that the label text should be rendered at
+ * @param textOptions the paper.PointText options object to customize the text properties
+ * @param maxWidth the maximum width of the label (text will be truncated with an ellipsis if the max width is
+ * exceeded)
+ *
+ * @example
+ * // basic configuration
+ * const text = new LabelTextComponent('Hello', new paper.Point(10, 30));
+ *
+ * @example
+ * // with text options
+ * const textOptions = {
+ * fontWeight: 'bold',
+ * justification: 'right',
+ * fillColor: new paper.Color('blue'),
+ * fontSize: 30,
+ * leading: 35
+ * }
+ * const fancyText = new LabelTextComponent('Salutations', new paper.Point(10, 5), textOptions);
+ */
+ constructor(protected _text: string,
+ protected _point: paper.Point = new paper.Point(0, 0),
+ protected textOptions: TextOptions = {},
+ protected maxWidth = DEFAULT_MAX_LABEL_WIDTH) {
+ super();
+ this.pivot = new paper.Point(0, 0);
+ this.position = _point;
+
+ this._label = new paper.PointText({
+ pivot: new paper.Point(0, 0),
+ justification: 'left',
+ fillColor: TEXT_COLOR,
+ fontSize: FONT_SIZE,
+ leading: LINE_HEIGHT,
+ content: _text,
+ ...textOptions,
+ parent: this
+ });
+
+ this.clip();
+ }
+
+ /**
+ * Gets the text component.
+ */
+ get text(): paper.PointText {
+ return this._label;
+ }
+
+ /**
+ * Clips the text and inserts an ellipsis to ensure that the max width is not exceeded.
+ */
+ private clip() {
+ let clipped = false;
+ while (this._label.bounds.width > this.maxWidth) {
+ clipped = true;
+ this._label.content = this._label.content.substring(0, this._label.content.length - 1);
+ }
+ if (clipped) {
+ this._label.content = this._label.content.substring(0, this._label.content.length - 3) + '...';
+ }
+ }
+}
diff --git a/src/components/label.test.ts b/src/components/label.test.ts
new file mode 100644
index 0000000..34f59c0
--- /dev/null
+++ b/src/components/label.test.ts
@@ -0,0 +1,39 @@
+import { LabelComponent } from './label';
+import * as paper from 'paper';
+
+describe('entity label component', () => {
+
+ beforeAll(() => {
+ const canvasEl = document.createElement('canvas');
+ paper.setup(canvasEl);
+ paper.settings.applyMatrix = false;
+ });
+
+ test('basic properties', () => {
+ const text = 'foobarbaz';
+ const position = new paper.Point(0, 0);
+ const label = new LabelComponent(text);
+ expect(label.position.x).toBe(position.x);
+ expect(label.position.y).toBe(position.y);
+ expect(label.text.content).toBe(text);
+ });
+
+ test('background component', () => {
+ const text = 'hello world';
+ const position = new paper.Point(2, 448);
+ const label = new LabelComponent(text, position);
+ const styleOptions = {
+ fillColor: new paper.Color('green'),
+ strokeWidth: 2,
+ strokeColor: new paper.Color('pink')
+ };
+ label.background.style = styleOptions;
+ expect(label.position.x).toBe(position.x);
+ expect(label.position.y).toBe(position.y);
+ expect(label.text.content).toBe(text);
+ expect(label.background.fillColor).toBe(styleOptions.fillColor);
+ expect(label.background.strokeWidth).toBe(styleOptions.strokeWidth);
+ expect(label.background.strokeColor).toBe(styleOptions.strokeColor);
+ });
+
+});
diff --git a/src/components/label.ts b/src/components/label.ts
index b42a3a5..91a1e81 100644
--- a/src/components/label.ts
+++ b/src/components/label.ts
@@ -1,25 +1,21 @@
import * as paper from 'paper';
-import { LABEL_HORIZONTAL_PADDING } from '../constants/dimensions';
+import { LABEL_HORIZONTAL_PADDING, LABEL_HEIGHT, DEFAULT_MAX_LABEL_WIDTH } from '../constants/dimensions';
+import { LabelTextComponent, TextOptions, FONT_SIZE } from './label-text';
+import { WHITE, CANVAS_BACKGROUND_COLOR } from '../constants/colors';
-const HEIGHT = 30;
-const TEXT_COLOR = '#FFFFFF';
-const TEXT_HOVER_COLOR = '#FFFFFF';
+const TEXT_COLOR = WHITE;
+const TEXT_HOVER_COLOR = WHITE;
const ACTIVE_TEXT_COLOR = '#252A3A';
-const ACTIVE_BACKGROUND_COLOR = '#FFFFFF';
-const BACKGROUND_COLOR = '#191C28';
+const ACTIVE_BACKGROUND_COLOR = WHITE;
+const BACKGROUND_COLOR = CANVAS_BACKGROUND_COLOR;
const HOVER_BACKGROUND_COLOR = '#242A3B';
export const VERTICAL_PADDING_TOP = 6;
-export const FONT_SIZE = 13;
-const LINE_HEIGHT = 15;
-const DEFAULT_MAX_LABEL_WIDTH = 200;
/**
- * LabelComponent Visual Component.
+ * Label Visual Component.
*/
-export class LabelComponent extends paper.Group {
+export class LabelComponent extends LabelTextComponent {
- // the text label
- protected _label: paper.PointText;
// the background item
protected _background: paper.Path.Rectangle;
@@ -28,45 +24,35 @@ export class LabelComponent extends paper.Group {
*
* @param _text the text to be displayed on the label
* @param _point the location to render the label at
+ * @param textOptions the paper.PointText options object to customize the text properties
* @param maxWidth the maximum width of the label (text will be truncated with an ellipsis if the max width is
* exceeded)
*/
- constructor(protected _text: string, protected _point: paper.Point = new paper.Point(0, 0),
+ constructor(protected _text: string,
+ protected _point: paper.Point = new paper.Point(0, 0),
+ protected textOptions: TextOptions = {},
protected maxWidth = DEFAULT_MAX_LABEL_WIDTH) {
- super();
- this.applyMatrix = false;
+ super(_text, _point, textOptions, maxWidth);
+ this.pivot = new paper.Point(0, 0);
this.position = _point;
- this._label = new paper.PointText(new paper.Point(LABEL_HORIZONTAL_PADDING, VERTICAL_PADDING_TOP +
- FONT_SIZE));
- this._label.justification = 'left';
- this._label.fillColor = TEXT_COLOR;
- this._label.content = _text;
- this._label.fontSize = FONT_SIZE;
- this._label.leading = LINE_HEIGHT;
- this._label.pivot = new paper.Point(0, 0);
- this.clip();
+
+ this._label.position = new paper.Point(LABEL_HORIZONTAL_PADDING, VERTICAL_PADDING_TOP + FONT_SIZE);
+
this._background = new paper.Path.Rectangle({
rectangle: new paper.Rectangle(0, 0,
- this._label.bounds.width + (LABEL_HORIZONTAL_PADDING * 2), HEIGHT),
- radius: 3
+ this._label.bounds.width + (LABEL_HORIZONTAL_PADDING * 2), LABEL_HEIGHT),
+ radius: 3,
+ fillColor: BACKGROUND_COLOR,
+ pivot: new paper.Point(0, 0)
});
- this._background.fillColor = BACKGROUND_COLOR;
- this._background.pivot = new paper.Point(0, 0);
- this.addChild(this._background);
- this.addChild(this._label);
- }
- /**
- * Gets the label text component.
- */
- getTextComponent(): paper.PointText {
- return this._label;
+ this.addChildren([this._background, this._label]);
}
/**
* Gets the label background component.
*/
- getBackgroundComponent(): paper.Path.Rectangle {
+ get background(): paper.Path.Rectangle {
return this._background;
}
@@ -87,25 +73,11 @@ export class LabelComponent extends paper.Group {
}
/**
- * Sets the label to its visual active state;
+ * Sets the label to its visual active state.
*/
setActive() {
this._label.fillColor = ACTIVE_TEXT_COLOR;
this._background.fillColor = ACTIVE_BACKGROUND_COLOR;
}
- /**
- * Clips the text and inserts an ellipsis to ensure that the max width is not exceeded.
- */
- private clip() {
- let clipped = false;
- while (this._label.bounds.width > this.maxWidth) {
- clipped = true;
- this._label.content = this._label.content.substring(0, this._label.content.length - 1);
- }
- if (clipped) {
- this._label.content = this._label.content.substring(0, this._label.content.length - 3) + '...';
- }
- }
-
}
diff --git a/src/components/margin.test.ts b/src/components/margin.test.ts
new file mode 100644
index 0000000..c8407eb
--- /dev/null
+++ b/src/components/margin.test.ts
@@ -0,0 +1,379 @@
+import { MarginComponent, MarginValues } from './margin';
+import * as paper from 'paper';
+
+describe('margin component', () => {
+
+ beforeAll(() => {
+ const canvasEl = document.createElement('canvas');
+ paper.setup(canvasEl);
+ paper.settings.applyMatrix = false;
+ });
+
+ function createContent() {
+ return new paper.Path.Rectangle({
+ pivot: new paper.Point(0, 0),
+ position: new paper.Point(0, 0),
+ size: new paper.Size(100, 100)
+ });
+ }
+
+ test('basic default properties', () => {
+ const content = createContent();
+ const margin = new MarginComponent(content);
+ expect(margin.bounds.size.height).toBe(content.bounds.height);
+ expect(margin.bounds.size.width).toBe(content.bounds.width);
+ expect(margin.bounds.top).toBe(content.bounds.top);
+ expect(margin.bounds.right).toBe(content.bounds.right);
+ expect(margin.bounds.bottom).toBe(content.bounds.bottom);
+ expect(margin.bounds.left).toBe(content.bounds.left);
+ expect(margin.position.x).toBe(content.position.x);
+ expect(margin.position.y).toBe(content.position.y);
+ });
+
+ test('initialized with a MarginValues instance', () => {
+ const content = createContent();
+ const marginValue: MarginValues = {
+ top: 5,
+ right: 21,
+ bottom: 74,
+ left: 12
+ };
+ const margin = new MarginComponent(content, marginValue);
+ expect(margin.bounds.size.height).toBe(content.bounds.height + marginValue.top + marginValue.bottom);
+ expect(margin.bounds.size.width).toBe(content.bounds.width + marginValue.right + marginValue.left);
+ expect(margin.bounds.top).toBe(content.bounds.top - marginValue.top);
+ expect(margin.bounds.right).toBe(content.bounds.right + marginValue.right);
+ expect(margin.bounds.bottom).toBe(content.bounds.bottom + marginValue.bottom);
+ expect(margin.bounds.left).toBe(content.bounds.left - marginValue.left);
+ expect(margin.position.x).toBe(content.position.x - marginValue.left);
+ expect(margin.position.y).toBe(content.position.y - marginValue.top);
+ });
+
+ test('initialized with a top and left partial MarginValues instance', () => {
+ const content = createContent();
+ const marginValue: Partial = {
+ top: 25,
+ left: 95
+ };
+ const testValue = marginValue as any; // to avoid object is possibly undefined error
+ const margin = new MarginComponent(content, marginValue);
+ expect(margin.bounds.size.height).toBe(content.bounds.height + testValue.top);
+ expect(margin.bounds.size.width).toBe(content.bounds.width + testValue.left);
+ expect(margin.bounds.top).toBe(content.bounds.top - testValue.top);
+ expect(margin.bounds.right).toBe(content.bounds.right);
+ expect(margin.bounds.bottom).toBe(content.bounds.bottom);
+ expect(margin.bounds.left).toBe(content.bounds.left - testValue.left);
+ expect(margin.position.x).toBe(content.position.x - testValue.left);
+ expect(margin.position.y).toBe(content.position.y - testValue.top);
+ });
+
+ test('initialized with a right and bottom partial MarginValues instance', () => {
+ const content = createContent();
+ const marginValue: Partial = {
+ right: 51,
+ bottom: 32
+ };
+ const testValue = marginValue as any;
+ const margin = new MarginComponent(content, marginValue);
+ expect(margin.bounds.size.height).toBe(content.bounds.height + testValue.bottom);
+ expect(margin.bounds.size.width).toBe(content.bounds.width + testValue.right);
+ expect(margin.bounds.top).toBe(content.bounds.top);
+ expect(margin.bounds.right).toBe(content.bounds.right + testValue.right);
+ expect(margin.bounds.bottom).toBe(content.bounds.bottom + testValue.bottom);
+ expect(margin.bounds.left).toBe(content.bounds.left);
+ expect(margin.position.x).toBe(content.position.x);
+ expect(margin.position.y).toBe(content.position.y);
+ });
+
+ test('initialized with four values in css shorthand notation', () => {
+ const content = createContent();
+ const marginValue = {
+ top: 10,
+ right: 20,
+ bottom: 15,
+ left: 30
+ };
+ const margin = new MarginComponent(
+ content, marginValue.top, marginValue.right, marginValue.bottom, marginValue.left);
+ expect(margin.bounds.size.height).toBe(content.bounds.height + marginValue.top + marginValue.bottom);
+ expect(margin.bounds.size.width).toBe(content.bounds.width + marginValue.right + marginValue.left);
+ expect(margin.bounds.top).toBe(content.bounds.top - marginValue.top);
+ expect(margin.bounds.right).toBe(content.bounds.right + marginValue.right);
+ expect(margin.bounds.bottom).toBe(content.bounds.bottom + marginValue.bottom);
+ expect(margin.bounds.left).toBe(content.bounds.left - marginValue.left);
+ expect(margin.position.x).toBe(content.position.x - marginValue.left);
+ expect(margin.position.y).toBe(content.position.y - marginValue.top);
+ });
+
+ test('initialized with one value in css shorthand notation', () => {
+ const content = createContent();
+ const marginValue = 20;
+ const margin = new MarginComponent(content, marginValue);
+ expect(margin.bounds.size.height).toBe(content.bounds.height + marginValue * 2);
+ expect(margin.bounds.size.width).toBe(content.bounds.width + marginValue * 2);
+ expect(margin.bounds.top).toBe(content.bounds.top - marginValue);
+ expect(margin.bounds.right).toBe(content.bounds.right + marginValue);
+ expect(margin.bounds.bottom).toBe(content.bounds.bottom + marginValue);
+ expect(margin.bounds.left).toBe(content.bounds.left - marginValue);
+ expect(margin.position.x).toBe(content.position.x - marginValue);
+ expect(margin.position.y).toBe(content.position.y - marginValue);
+ });
+
+ test('initialized with two values in css shorthand notation', () => {
+ const content = createContent();
+ const marginValue = {
+ vertical: 10,
+ horizontal: 20
+ };
+ const margin = new MarginComponent(content, marginValue.vertical, marginValue.horizontal);
+ expect(margin.bounds.size.height).toBe(content.bounds.height + marginValue.vertical * 2);
+ expect(margin.bounds.size.width).toBe(content.bounds.width + marginValue.horizontal * 2);
+ expect(margin.bounds.top).toBe(content.bounds.top - marginValue.vertical);
+ expect(margin.bounds.right).toBe(content.bounds.right + marginValue.horizontal);
+ expect(margin.bounds.bottom).toBe(content.bounds.bottom + marginValue.vertical);
+ expect(margin.bounds.left).toBe(content.bounds.left - marginValue.horizontal);
+ expect(margin.position.x).toBe(content.position.x - marginValue.horizontal);
+ expect(margin.position.y).toBe(content.position.y - marginValue.vertical);
+ });
+
+ test('initialized with three values in css shorthand notation', () => {
+ const content = createContent();
+ const marginValue = {
+ top: 10,
+ horizontal: 20,
+ bottom: 15
+ };
+ const margin = new MarginComponent(content, marginValue.top, marginValue.horizontal, marginValue.bottom);
+ expect(margin.bounds.size.height).toBe(content.bounds.height + marginValue.top + marginValue.bottom);
+ expect(margin.bounds.size.width).toBe(content.bounds.width + marginValue.horizontal * 2);
+ expect(margin.bounds.top).toBe(content.bounds.top - marginValue.top);
+ expect(margin.bounds.right).toBe(content.bounds.right + marginValue.horizontal);
+ expect(margin.bounds.bottom).toBe(content.bounds.bottom + marginValue.bottom);
+ expect(margin.bounds.left).toBe(content.bounds.left - marginValue.horizontal);
+ expect(margin.position.x).toBe(content.position.x - marginValue.horizontal);
+ expect(margin.position.y).toBe(content.position.y - marginValue.top);
+ });
+
+ test('initialized with four values in css shorthand notation', () => {
+ const content = createContent();
+ const marginValue = {
+ top: 10,
+ right: 20,
+ bottom: 15,
+ left: 30
+ };
+ const margin = new MarginComponent(
+ content, marginValue.top, marginValue.right, marginValue.bottom, marginValue.left);
+ expect(margin.bounds.size.height).toBe(content.bounds.height + marginValue.top + marginValue.bottom);
+ expect(margin.bounds.size.width).toBe(content.bounds.width + marginValue.right + marginValue.left);
+ expect(margin.bounds.top).toBe(content.bounds.top - marginValue.top);
+ expect(margin.bounds.right).toBe(content.bounds.right + marginValue.right);
+ expect(margin.bounds.bottom).toBe(content.bounds.bottom + marginValue.bottom);
+ expect(margin.bounds.left).toBe(content.bounds.left - marginValue.left);
+ expect(margin.position.x).toBe(content.position.x - marginValue.left);
+ expect(margin.position.y).toBe(content.position.y - marginValue.top);
+ });
+
+ test('setValues with a MarginValues instance', () => {
+ const content = createContent();
+ const originalMarginValue = {
+ top: 62,
+ right: 5,
+ bottom: 44,
+ left: 67
+ };
+ const marginValue: MarginValues = {
+ top: 7,
+ right: 37,
+ bottom: 1,
+ left: 64
+ };
+ const deltaTop = marginValue.top - originalMarginValue.top;
+ const deltaLeft = marginValue.left - originalMarginValue.left;
+ const margin = new MarginComponent(content,
+ originalMarginValue.top, originalMarginValue.right, originalMarginValue.bottom, originalMarginValue.left);
+ margin.setValues(marginValue);
+ expect(margin.bounds.size.height).toBe(content.bounds.height + marginValue.top + marginValue.bottom);
+ expect(margin.bounds.size.width).toBe(content.bounds.width + marginValue.right + marginValue.left);
+ expect(margin.bounds.top).toBe(content.bounds.top - marginValue.top - deltaTop);
+ expect(margin.bounds.right).toBe(content.bounds.right + marginValue.right - deltaLeft);
+ expect(margin.bounds.bottom).toBe(content.bounds.bottom + marginValue.bottom - deltaTop);
+ expect(margin.bounds.left).toBe(content.bounds.left - marginValue.left - deltaLeft);
+ expect(margin.position.x).toBe(content.position.x - marginValue.left - deltaLeft);
+ expect(margin.position.y).toBe(content.position.y - marginValue.top - deltaTop);
+ });
+
+ test('setValues with a top and left partial MarginValues instance', () => {
+ const content = createContent();
+ const originalMarginValue = {
+ top: 73,
+ right: 8,
+ bottom: 90,
+ left: 42
+ };
+ const marginValue: Partial = {
+ top: 22,
+ left: 51
+ };
+ const testValue = marginValue as any;
+ const deltaTop = testValue.top - originalMarginValue.top;
+ const deltaLeft = testValue.left - originalMarginValue.left;
+ const margin = new MarginComponent(content,
+ originalMarginValue.top, originalMarginValue.right, originalMarginValue.bottom, originalMarginValue.left);
+ margin.setValues(marginValue);
+ expect(margin.bounds.size.height).toBe(content.bounds.height + testValue.top + originalMarginValue.bottom);
+ expect(margin.bounds.size.width).toBe(content.bounds.width + originalMarginValue.right + testValue.left);
+ expect(margin.bounds.top).toBe(content.bounds.top - testValue.top - deltaTop);
+ expect(margin.bounds.right).toBe(content.bounds.right + originalMarginValue.right - deltaLeft);
+ expect(margin.bounds.bottom).toBe(content.bounds.bottom + originalMarginValue.bottom - deltaTop);
+ expect(margin.bounds.left).toBe(content.bounds.left - testValue.left - deltaLeft);
+ expect(margin.position.x).toBe(content.position.x - testValue.left - deltaLeft);
+ expect(margin.position.y).toBe(content.position.y - testValue.top - deltaTop);
+ });
+
+ test('setValues with a right and bottom partial MarginValues instance', () => {
+ const content = createContent();
+ const originalMarginValue = {
+ top: 26,
+ right: 25,
+ bottom: 6,
+ left: 2
+ };
+ const marginValue: Partial = {
+ right: 10,
+ bottom: 10
+ };
+ const testValue = marginValue as any;
+ const deltaTop = originalMarginValue.top - originalMarginValue.top;
+ const deltaLeft = originalMarginValue.left - originalMarginValue.left;
+ const margin = new MarginComponent(content,
+ originalMarginValue.top, originalMarginValue.right, originalMarginValue.bottom, originalMarginValue.left);
+ margin.setValues(marginValue);
+ expect(margin.bounds.size.height).toBe(content.bounds.height + originalMarginValue.top + testValue.bottom);
+ expect(margin.bounds.size.width).toBe(content.bounds.width + testValue.right + originalMarginValue.left);
+ expect(margin.bounds.top).toBe(content.bounds.top - originalMarginValue.top - deltaTop);
+ expect(margin.bounds.right).toBe(content.bounds.right + testValue.right - deltaLeft);
+ expect(margin.bounds.bottom).toBe(content.bounds.bottom + testValue.bottom - deltaTop);
+ expect(margin.bounds.left).toBe(content.bounds.left - originalMarginValue.left - deltaLeft);
+ expect(margin.position.x).toBe(content.position.x - originalMarginValue.left - deltaLeft);
+ expect(margin.position.y).toBe(content.position.y - originalMarginValue.top - deltaTop);
+ });
+
+ test('setValues with css shorthand notation', () => {
+ const content = createContent();
+ const originalMarginValue = {
+ top: 10,
+ right: 20,
+ bottom: 15,
+ left: 30
+ };
+ const marginValue = {
+ top: 22,
+ right: 63,
+ bottom: 35,
+ left: 3
+ };
+ const deltaTop = marginValue.top - originalMarginValue.top;
+ const deltaLeft = marginValue.left - originalMarginValue.left;
+ const margin = new MarginComponent(content,
+ originalMarginValue.top, originalMarginValue.right, originalMarginValue.bottom, originalMarginValue.left);
+ margin.setValues(marginValue.top, marginValue.right, marginValue.bottom, marginValue.left);
+ expect(margin.bounds.size.height).toBe(content.bounds.height + marginValue.top + marginValue.bottom);
+ expect(margin.bounds.size.width).toBe(content.bounds.width + marginValue.right + marginValue.left);
+ expect(margin.bounds.top).toBe(content.bounds.top - marginValue.top - deltaTop);
+ expect(margin.bounds.right).toBe(content.bounds.right + marginValue.right - deltaLeft);
+ expect(margin.bounds.bottom).toBe(content.bounds.bottom + marginValue.bottom - deltaTop);
+ expect(margin.bounds.left).toBe(content.bounds.left - marginValue.left - deltaLeft);
+ expect(margin.position.x).toBe(content.position.x - marginValue.left - deltaLeft);
+ expect(margin.position.y).toBe(content.position.y - marginValue.top - deltaTop);
+ });
+
+ test('set top', () => {
+ const marginValue = {
+ top: 10,
+ right: 20,
+ bottom: 15,
+ left: 30
+ };
+ const newValue = 3;
+ const margin = new MarginComponent(
+ createContent(), marginValue.top, marginValue.right, marginValue.bottom, marginValue.left);
+ margin.top = newValue;
+ const content = createContent();
+ expect(margin.bounds.size.height).toBe(content.bounds.height + newValue + marginValue.bottom);
+ expect(margin.bounds.size.width).toBe(content.bounds.width + marginValue.right + marginValue.left);
+ expect(margin.bounds.top).toBe(content.bounds.top - newValue);
+ expect(margin.bounds.right).toBe(content.bounds.right + marginValue.right);
+ expect(margin.bounds.bottom).toBe(content.bounds.bottom + marginValue.bottom);
+ expect(margin.bounds.left).toBe(content.bounds.left - marginValue.left);
+ expect(margin.position.x).toBe(content.position.x - marginValue.left);
+ expect(margin.position.y).toBe(content.position.y - newValue);
+ });
+
+ test('set right', () => {
+ const marginValue = {
+ top: 10,
+ right: 20,
+ bottom: 15,
+ left: 30
+ };
+ const newValue = 8;
+ const margin = new MarginComponent(
+ createContent(), marginValue.top, marginValue.right, marginValue.bottom, marginValue.left);
+ margin.right = newValue;
+ const content = createContent();
+ expect(margin.bounds.size.height).toBe(content.bounds.height + marginValue.top + marginValue.bottom);
+ expect(margin.bounds.size.width).toBe(content.bounds.width + newValue + marginValue.left);
+ expect(margin.bounds.top).toBe(content.bounds.top - marginValue.top);
+ expect(margin.bounds.right).toBe(content.bounds.right + newValue);
+ expect(margin.bounds.bottom).toBe(content.bounds.bottom + marginValue.bottom);
+ expect(margin.bounds.left).toBe(content.bounds.left - marginValue.left);
+ expect(margin.position.x).toBe(content.position.x - marginValue.left);
+ expect(margin.position.y).toBe(content.position.y - marginValue.top);
+ });
+
+ test('set bottom', () => {
+ const marginValue = {
+ top: 10,
+ right: 20,
+ bottom: 15,
+ left: 30
+ };
+ const newValue = 8;
+ const margin = new MarginComponent(
+ createContent(), marginValue.top, marginValue.right, marginValue.bottom, marginValue.left);
+ margin.bottom = newValue;
+ const content = createContent();
+ expect(margin.bounds.size.height).toBe(content.bounds.height + marginValue.top + newValue);
+ expect(margin.bounds.size.width).toBe(content.bounds.width + marginValue.right + marginValue.left);
+ expect(margin.bounds.top).toBe(content.bounds.top - marginValue.top);
+ expect(margin.bounds.right).toBe(content.bounds.right + marginValue.right);
+ expect(margin.bounds.bottom).toBe(content.bounds.bottom + newValue);
+ expect(margin.bounds.left).toBe(content.bounds.left - marginValue.left);
+ expect(margin.position.x).toBe(content.position.x - marginValue.left);
+ expect(margin.position.y).toBe(content.position.y - marginValue.top);
+ });
+
+ test('set left', () => {
+ const marginValue = {
+ top: 10,
+ right: 20,
+ bottom: 15,
+ left: 30
+ };
+ const newValue = 8;
+ const margin = new MarginComponent(
+ createContent(), marginValue.top, marginValue.right, marginValue.bottom, marginValue.left);
+ margin.left = newValue;
+ const content = createContent();
+ expect(margin.bounds.size.height).toBe(content.bounds.height + marginValue.top + marginValue.bottom);
+ expect(margin.bounds.size.width).toBe(content.bounds.width + marginValue.right + newValue);
+ expect(margin.bounds.top).toBe(content.bounds.top - marginValue.top);
+ expect(margin.bounds.right).toBe(content.bounds.right + marginValue.right);
+ expect(margin.bounds.bottom).toBe(content.bounds.bottom + marginValue.bottom);
+ expect(margin.bounds.left).toBe(content.bounds.left - newValue);
+ expect(margin.position.x).toBe(content.position.x - newValue);
+ expect(margin.position.y).toBe(content.position.y - marginValue.top);
+ });
+
+});
diff --git a/src/components/margin.ts b/src/components/margin.ts
new file mode 100644
index 0000000..3019c48
--- /dev/null
+++ b/src/components/margin.ts
@@ -0,0 +1,252 @@
+import * as paper from 'paper';
+
+const DEFAULT_MARGIN = 0;
+
+/**
+ * Interface for margin values.
+ */
+export class MarginValues {
+ top: number;
+ right: number;
+ bottom: number;
+ left: number;
+}
+
+/**
+ * Margin Component.
+ */
+export class MarginComponent extends paper.Group {
+
+ private marginValues: MarginValues = {
+ top: DEFAULT_MARGIN,
+ right: DEFAULT_MARGIN,
+ bottom: DEFAULT_MARGIN,
+ left: DEFAULT_MARGIN
+ };
+ // the surrounding margin rectangle
+ private margin: paper.Path.Rectangle;
+
+ /**
+ * Creates a new margin instance with a partial MarginValues instance.
+ *
+ * @param content The content that the margin surrounds.
+ * @param marginValues Values for the margin sides. Can set `top`, `right`, `bottom`, or `left` partially with
+ * an instance of MarginValues. Default is 0 for all margins.
+ *
+ * @example
+ * // all values assigned
+ * const marginValues: MarginValues = {
+ * top: 5,
+ * right: 21,
+ * bottom: 74,
+ * left: 12
+ * };
+ * const margin = new MarginComponent((your content), marginValues);
+ *
+ * @example
+ * // values partially assigned
+ * const marginValues: Partial = {
+ * top: 5,
+ * right: 21
+ * };
+ * const margin = new MarginComponent((your content), marginValues);
+ */
+ constructor(content: paper.Item | paper.Group, marginValues: Partial);
+ /**
+ * Creates a new margin instance with margin values written in CSS shorthand notation.
+ *
+ * @param content The content that the margin surrounds.
+ * @param marginValuesCssShorthand Values for the margin sides. Can have a series of 0 to 4 that are comma separated
+ * in CSS shorthand notation. Default is 0 for all margins.
+ *
+ * @example
+ * // initialized with all four values in css shorthand notation - top, right, bottom, left
+ * const margin = new MarginComponent((your content), 5, 21, 74, 12);
+ *
+ * @example
+ * // initialized with two values in css shorthand notation - top/bottom, sides
+ * const margin = new MarginComponent((your content), 10, 15);
+ */
+ constructor(content: paper.Item | paper.Group, ...marginValuesCssShorthand: number[]);
+ /**
+ * Creates a new margin instance.
+ *
+ * @param content The content that the margin surrounds.
+ * @param values Values for the margin side. Implementation of overloaded marginValues: Partial and
+ * marginValuesCssShorthand: number[].
+ */
+ constructor(private content: paper.Item | paper.Group, ...values: Partial[] | number[]) {
+ super();
+ this.pivot = new paper.Point(0, 0);
+
+ this.marginValues = this.assignMarginValues(values);
+ this.createAndPositionMargin();
+ }
+
+ /**
+ * Sets new margin values with a partial MarginValues instance.
+ *
+ * @param marginValues Values for the margin sides. Can override current `top`, `right`, `bottom`, or `left`
+ * values partially with an instance of MarginValues. Default is the original assignment from instantiation for
+ * all margins.
+ *
+ * @example
+ * // all values set
+ * const marginValues: MarginValues = {
+ * top: 5,
+ * right: 21,
+ * bottom: 74,
+ * left: 12
+ * };
+ * marginInstance.setValues(marginValues);
+ *
+ * @example
+ * // values partially set
+ * const marginValues: Partial = {
+ * top: 5,
+ * right: 21
+ * };
+ * marginInstance.setValues(marginValues);
+ */
+ setValues(marginValues: Partial): void;
+ /**
+ * Sets new margin values written in CSS shorthand notation.
+ *
+ * @param marginValuesCssShorthand Values for the margin sides. Can have a series of 0 to 4 that are comma separated
+ * in CSS shorthand notation. Default is 0 for all margins.
+ *
+ * @example
+ * // set all four values in css shorthand notation - top, right, bottom, left
+ * marginInstance.setValues(5, 21, 74, 12);
+ *
+ * @example
+ * // set with two values in css shorthand notation - top/bottom, sides
+ * marginInstance.setValues(10, 15);
+ */
+ setValues(...marginValuesCssShorthand: number[]): void;
+ /**
+ * Sets new margin values.
+ *
+ * @param values Values for the margin side. Implementation of overloaded marginValues: Partial and
+ * marginValuesCssShorthand: number[].
+ */
+ setValues(...values: Partial[] | number[]): void {
+ const previousTop = this.marginValues.top;
+ const previousLeft = this.marginValues.left;
+ this.marginValues = this.assignMarginValues(values);
+ this.rebuildMargin();
+ this.content.position.y += this.marginValues.top - previousTop;
+ this.content.position.x += this.marginValues.left - previousLeft;
+ }
+
+ /**
+ * Sets top margin value.
+ * @param newValue New value for top margin.
+ */
+ set top(newValue: number) {
+ const delta = newValue - this.marginValues.top;
+ this.marginValues.top = newValue;
+ this.rebuildMargin();
+ this.content.position.y += delta;
+ }
+
+ /**
+ * Sets right margin value.
+ * @param newValue New value for right margin.
+ */
+ set right(newValue: number) {
+ this.marginValues.right = newValue;
+ this.rebuildMargin();
+ }
+
+ /**
+ * Sets bottom margin value.
+ * @param newValue New value for bottom margin.
+ */
+ set bottom(newValue: number) {
+ this.marginValues.bottom = newValue;
+ this.rebuildMargin();
+ }
+
+ /**
+ * Sets left margin value.
+ * @param newValue New value for left margin.
+ */
+ set left(newValue: number) {
+ const delta = newValue - this.marginValues.left;
+ this.marginValues.left = newValue;
+ this.rebuildMargin();
+ this.content.position.x += delta;
+ }
+
+ /**
+ * Assigns margin values according to the type of the first value.
+ * @param values Margin values.
+ */
+ private assignMarginValues(values: Partial[] | number[]): MarginValues {
+ const firstValue = values[0];
+ return typeof firstValue === 'number'
+ ? this.assignCssStyleMarginValues(values as number[])
+ : { ...this.marginValues, ...firstValue };
+ }
+
+ /**
+ * Assigns margin values based on CSS shorthand notation.
+ * @param values Margin values.
+ */
+ private assignCssStyleMarginValues(values: number[]): MarginValues {
+ let top;
+ let right;
+ let bottom;
+ let left;
+ switch (values.length) {
+ case 4:
+ [top, right, bottom, left] = values;
+ break;
+ case 3:
+ [top, right, bottom] = values;
+ left = right;
+ break;
+ case 2:
+ [top, right] = values;
+ bottom = top;
+ left = right;
+ break;
+ case 1:
+ top = right = bottom = left = values[0];
+ break;
+ default:
+ top = right = bottom = left = DEFAULT_MARGIN;
+ }
+
+ return {
+ top: top,
+ right: right,
+ bottom: bottom,
+ left: left
+ };
+ }
+
+ /**
+ * Creates and positions margin.
+ */
+ private createAndPositionMargin(): void {
+ this.margin = new paper.Path.Rectangle({
+ pivot: new paper.Point(0, 0),
+ position: this.content.bounds.topLeft,
+ size: this.content.bounds.size.add(new paper.Size(
+ this.marginValues.left + this.marginValues.right,
+ this.marginValues.top + this.marginValues.bottom)),
+ parent: this
+ });
+ this.position = new paper.Point(-this.marginValues.left, -this.marginValues.top);
+ }
+
+ /**
+ * Rebuilds margin.
+ */
+ private rebuildMargin(): void {
+ this.margin.remove();
+ this.createAndPositionMargin();
+ }
+}
diff --git a/src/components/scrollbar.test.ts b/src/components/scrollbar.test.ts
index 81dc5c9..b0dd023 100644
--- a/src/components/scrollbar.test.ts
+++ b/src/components/scrollbar.test.ts
@@ -3,17 +3,16 @@ import { ScrollbarComponent } from './scrollbar';
describe('scrollbar component', () => {
- beforeEach(() => {
+ beforeAll(() => {
const canvasEl = document.createElement('canvas');
paper.setup(canvasEl);
+ paper.settings.applyMatrix = false;
});
test('basic properties and defaults', () => {
const scrollable = {
- content: new paper.Path.Rectangle(
- new paper.Point(10, 0),
- new paper.Size(1000, 500)
- ),
+ content: new paper.Path.Rectangle(new paper.Rectangle(10, 0, 1000, 500)),
+ container: new paper.Path.Rectangle(new paper.Rectangle(0, 0, 500, 500)),
containerBounds: new paper.Rectangle(0, 0, 500, 500)
};
const defaultPosition = new paper.Point(0, 0);
@@ -31,10 +30,8 @@ describe('scrollbar component', () => {
test('basic properties and custom position', () => {
const scrollable = {
- content: new paper.Path.Rectangle(
- new paper.Point(10, 0),
- new paper.Size(1000, 500)
- ),
+ content: new paper.Path.Rectangle(new paper.Rectangle(10, 0, 1000, 500)),
+ container: new paper.Path.Rectangle(new paper.Rectangle(0, 0, 500, 500)),
containerBounds: new paper.Rectangle(0, 0, 500, 500)
};
const customPosition = new paper.Point(30, 30);
@@ -53,10 +50,8 @@ describe('scrollbar component', () => {
test('basic properties and custom scrollTrackLength', () => {
const scrollable = {
- content: new paper.Path.Rectangle(
- new paper.Point(10, 0),
- new paper.Size(1000, 500)
- ),
+ content: new paper.Path.Rectangle(new paper.Rectangle(10, 0, 1000, 500)),
+ container: new paper.Path.Rectangle(new paper.Rectangle(0, 0, 500, 500)),
containerBounds: new paper.Rectangle(0, 0, 500, 500)
};
const defaultPosition = new paper.Point(0, 0);
@@ -75,10 +70,8 @@ describe('scrollbar component', () => {
test('does not build when content fits inside container' , () => {
const scrollable = {
- content: new paper.Path.Rectangle(
- new paper.Point(0, 0),
- new paper.Size(500, 500)
- ),
+ content: new paper.Path.Rectangle(new paper.Rectangle(0, 0, 500, 500)),
+ container: new paper.Path.Rectangle(new paper.Rectangle(0, 0, 500, 500)),
containerBounds: new paper.Rectangle(0, 0, 500, 500)
};
const scrollbar = new ScrollbarComponent(scrollable, new paper.Point(0, 0));
@@ -87,10 +80,8 @@ describe('scrollbar component', () => {
test('builds when content does not fit inside container' , () => {
const scrollable = {
- content: new paper.Path.Rectangle(
- new paper.Point(0, 0),
- new paper.Size(501, 500)
- ),
+ content: new paper.Path.Rectangle(new paper.Rectangle(0, 0, 501, 500)),
+ container: new paper.Path.Rectangle(new paper.Rectangle(0, 0, 500, 500)),
containerBounds: new paper.Rectangle(0, 0, 500, 500)
};
const scrollbar = new ScrollbarComponent(scrollable, new paper.Point(0, 0));
@@ -99,10 +90,8 @@ describe('scrollbar component', () => {
test('does not build when content including offsets do not fit but checkFitWithOffsets is off' , () => {
const scrollable = {
- content: new paper.Path.Rectangle(
- new paper.Point(0, 0),
- new paper.Size(500, 500)
- ),
+ content: new paper.Path.Rectangle(new paper.Rectangle(0, 0, 500, 500)),
+ container: new paper.Path.Rectangle(new paper.Rectangle(0, 0, 500, 500)),
containerBounds: new paper.Rectangle(0, 0, 500, 500),
contentOffsetStart: 10,
contentOffsetEnd: 10,
@@ -114,10 +103,8 @@ describe('scrollbar component', () => {
test('builds when content including offsets do not fit and checkFitWithOffsets is on' , () => {
const scrollable = {
- content: new paper.Path.Rectangle(
- new paper.Point(0, 0),
- new paper.Size(500, 500)
- ),
+ content: new paper.Path.Rectangle(new paper.Rectangle(0, 0, 500, 500)),
+ container: new paper.Path.Rectangle(new paper.Rectangle(0, 0, 500, 500)),
containerBounds: new paper.Rectangle(0, 0, 500, 500),
contentOffsetStart: 10,
contentOffsetEnd: 10,
@@ -129,10 +116,8 @@ describe('scrollbar component', () => {
test('getScrollbar returns child scrollbar when component is enabled' , () => {
const scrollable = {
- content: new paper.Path.Rectangle(
- new paper.Point(0, 0),
- new paper.Size(1000, 500)
- ),
+ content: new paper.Path.Rectangle(new paper.Rectangle(0, 0, 1000, 500)),
+ container: new paper.Path.Rectangle(new paper.Rectangle(0, 0, 500, 500)),
containerBounds: new paper.Rectangle(0, 0, 500, 500)
};
const scrollbar = new ScrollbarComponent(scrollable, new paper.Point(0, 0));
@@ -143,10 +128,8 @@ describe('scrollbar component', () => {
test('getScrollbar returns scrollbar that\'s not a child when component is disabled' , () => {
const scrollable = {
- content: new paper.Path.Rectangle(
- new paper.Point(0, 0),
- new paper.Size(500, 500)
- ),
+ content: new paper.Path.Rectangle(new paper.Rectangle(0, 0, 500, 500)),
+ container: new paper.Path.Rectangle(new paper.Rectangle(0, 0, 500, 500)),
containerBounds: new paper.Rectangle(0, 0, 500, 500)
};
const scrollbar = new ScrollbarComponent(scrollable, new paper.Point(0, 0));
@@ -157,10 +140,8 @@ describe('scrollbar component', () => {
test('getScrollbar allows changes to scrollbar attributes' , () => {
const scrollable = {
- content: new paper.Path.Rectangle(
- new paper.Point(0, 0),
- new paper.Size(500, 500)
- ),
+ content: new paper.Path.Rectangle(new paper.Rectangle(0, 0, 500, 500)),
+ container: new paper.Path.Rectangle(new paper.Rectangle(0, 0, 500, 500)),
containerBounds: new paper.Rectangle(0, 0, 500, 500)
};
const scrollbar = new ScrollbarComponent(scrollable, new paper.Point(0, 0));
@@ -177,10 +158,8 @@ describe('scrollbar component', () => {
test('setScrollbar allows fuller changes to scrollbar' , () => {
const scrollable = {
- content: new paper.Path.Rectangle(
- new paper.Point(0, 0),
- new paper.Size(500, 500)
- ),
+ content: new paper.Path.Rectangle(new paper.Rectangle(0, 0, 500, 500)),
+ container: new paper.Path.Rectangle(new paper.Rectangle(0, 0, 500, 500)),
containerBounds: new paper.Rectangle(0, 0, 500, 500)
};
const scrollbar = new ScrollbarComponent(scrollable, new paper.Point(0, 0));
@@ -203,10 +182,8 @@ describe('scrollbar component', () => {
test('getTrack returns child track when component is enabled' , () => {
const scrollable = {
- content: new paper.Path.Rectangle(
- new paper.Point(0, 0),
- new paper.Size(501, 500)
- ),
+ content: new paper.Path.Rectangle(new paper.Rectangle(0, 0, 501, 500)),
+ container: new paper.Path.Rectangle(new paper.Rectangle(0, 0, 500, 500)),
containerBounds: new paper.Rectangle(0, 0, 500, 500)
};
const scrollbar = new ScrollbarComponent(scrollable, new paper.Point(0, 0));
@@ -217,10 +194,8 @@ describe('scrollbar component', () => {
test('getTrack returns track that\'s not a child when component is disabled' , () => {
const scrollable = {
- content: new paper.Path.Rectangle(
- new paper.Point(0, 0),
- new paper.Size(500, 500)
- ),
+ content: new paper.Path.Rectangle(new paper.Rectangle(0, 0, 500, 500)),
+ container: new paper.Path.Rectangle(new paper.Rectangle(0, 0, 500, 500)),
containerBounds: new paper.Rectangle(0, 0, 500, 500)
};
const scrollbar = new ScrollbarComponent(scrollable, new paper.Point(0, 0));
@@ -231,10 +206,8 @@ describe('scrollbar component', () => {
test('getTrack allows changes to track attributes' , () => {
const scrollable = {
- content: new paper.Path.Rectangle(
- new paper.Point(0, 0),
- new paper.Size(500, 500)
- ),
+ content: new paper.Path.Rectangle(new paper.Rectangle(0, 0, 500, 500)),
+ container: new paper.Path.Rectangle(new paper.Rectangle(0, 0, 500, 500)),
containerBounds: new paper.Rectangle(0, 0, 500, 500)
};
const scrollbar = new ScrollbarComponent(scrollable, new paper.Point(0, 0));
@@ -251,10 +224,8 @@ describe('scrollbar component', () => {
test('setTrack allows fuller changes to track' , () => {
const scrollable = {
- content: new paper.Path.Rectangle(
- new paper.Point(0, 0),
- new paper.Size(500, 500)
- ),
+ content: new paper.Path.Rectangle(new paper.Rectangle(0, 0, 500, 500)),
+ container: new paper.Path.Rectangle(new paper.Rectangle(0, 0, 500, 500)),
containerBounds: new paper.Rectangle(0, 0, 500, 500)
};
const scrollbar = new ScrollbarComponent(scrollable, new paper.Point(0, 0));
diff --git a/src/components/scrollbar.ts b/src/components/scrollbar.ts
index bbc3c0b..4d53447 100644
--- a/src/components/scrollbar.ts
+++ b/src/components/scrollbar.ts
@@ -10,6 +10,9 @@ const ACTIVE_SCROLLBAR_OPACITY = 1;
const HIT_TEST_TOLERANCE = 11;
const DEFAULT_SCROLLABLE_OFFSET = 0;
+/**
+ * Enumeration of scrollbar directions.
+ */
type ScrollbarDirection = 'horizontal' | 'vertical';
/**
@@ -21,8 +24,12 @@ interface Scrollable {
*/
content: paper.Group | paper.Item;
/**
- * @property containerBounds Bounds of the container that displays the viewable content. Could be something like
- * the view or a clip mask.
+ * @property container The container that displays the viewable content. Could be something like the view or a
+ * clip mask. Used for hover event listening. HTMLCanvasElement used to solve issues with multiple paper views.
+ */
+ container: paper.Group | paper.Item | paper.View | HTMLCanvasElement;
+ /**
+ * @property containerBounds Bounds of the container that displays the viewable content. Used for measurements.
*/
containerBounds: paper.Rectangle;
/**
@@ -55,6 +62,14 @@ class CustomEffects {
* Scrollbar Visual Component.
*/
export class ScrollbarComponent extends paper.Group {
+
+ // checks if any scrollbars are currently receiving mouse drag input to override other default events
+ static anyIsDragging: boolean = false;
+ // the default scrollbar that can still receive input events while a scrollbar of another direction is active. For
+ // example, the main view's horizontal scrollbar can scroll from horizontal input events even if a vapp vertical
+ // scrollbar is active.
+ static _defaultScrollbar: ScrollbarComponent | undefined;
+ static defaultScrollbarDirection: ScrollbarDirection | undefined;
protected scrollbar: paper.Path;
protected track: paper.Path;
protected scrollAmount: number;
@@ -67,15 +82,18 @@ export class ScrollbarComponent extends paper.Group {
private dimension: (keyof paper.Rectangle);
// content's original position. used to constrain content position while scrolling
private contentInitialPosition: number;
- // scrollbar is rendered and enabled when it's needed
- private isEnabled: boolean = true;
+ // scrollbar is visible and interactive when container is hovered
+ private enabled: boolean = true;
private containerSize: number;
+ // make a bigger area to make the track easier to interact with
private extendedTrackArea: paper.Path.Rectangle;
+ // make a bigger area to make the scrollbar easier to interact with
private extendedScrollbarArea: paper.Path.Rectangle;
private arrowKeyDown: paper.Tool;
private scrollbarDrag: paper.Tool;
private dragging: boolean = false;
- private hovering: boolean = false;
+ private scrollbarHovering: boolean = false;
+ private containerHovering: boolean = false;
private activeScrollTimeout: ReturnType;
/**
@@ -91,6 +109,7 @@ export class ScrollbarComponent extends paper.Group {
* // Create a horizontal scrollbar with mostly default configuration.
* const scrollable = {
* content: (your content),
+ * container: (content's container),
* containerBounds: (content's container.bounds)
* }
* const scrollbar = new ScrollbarComponent(scrollable, new paper.Point(30, 30));
@@ -100,6 +119,7 @@ export class ScrollbarComponent extends paper.Group {
* const containerPadding = 20;
* const scrollable = {
* content: (your content),
+ * container: (content's container),
* containerBounds: (content's container.bounds),
* contentOffsetEnd: containerPadding
* };
@@ -115,7 +135,6 @@ export class ScrollbarComponent extends paper.Group {
readonly scrollTrackLength: number = 0,
private direction: ScrollbarDirection = 'horizontal') {
super();
- this.applyMatrix = false;
this.position = this._point as paper.Point;
this.pivot = new paper.Point(0, 0);
@@ -132,11 +151,10 @@ export class ScrollbarComponent extends paper.Group {
this.track = this.createTrack(this.scrollTrackLength, DEFAULT_SCROLLBAR_THICKNESS);
this.scrollbar = this.createScrollbar(this.getProportionalLength(), DEFAULT_SCROLLBAR_THICKNESS);
- // extend track and scrollbar hit areas based on hit tolerance to make interaction easier. used for
- // onClick event and hover tests
+ // extend track and scrollbar hit areas based on hit tolerance to make interaction easier. used for onClick
+ // event and hover tests
this.extendedTrackArea = this.extendHitArea(this.track);
this.extendedScrollbarArea = this.extendHitArea(this.scrollbar);
-
// check if scrollbar is necessary
if (this.scrollableContentFitsContainer()) {
this.disable();
@@ -151,41 +169,87 @@ export class ScrollbarComponent extends paper.Group {
this.onClick = this.trackClick;
this.scrollbarDrag = this.scrollbarDragTool();
this.arrowKeyDown = this.arrowKeyDownTool();
+
+ // not visible until container is hovered and no other scrollbars are currently in dragging state
+ this.visible = false;
+ // handles hover events for html canvas and paper items
+ if (this.scrollable.container instanceof HTMLCanvasElement) {
+ this.scrollable.container.onmouseenter = () => this.containerMouseEnter();
+ this.scrollable.container.onmouseleave = () => this.containerMouseLeave();
+ } else {
+ this.scrollable.container.onMouseEnter = this.containerMouseEnter;
+ this.scrollable.container.onMouseLeave = this.containerMouseLeave;
+ }
+ }
+
+ /**
+ * Sets the default scrollbar.
+ * @param value The scrollbar that will be set as default.
+ */
+ static set defaultScrollbar(value: ScrollbarComponent) {
+ this._defaultScrollbar = value;
+ this.defaultScrollbarDirection = this._defaultScrollbar.isHorizontal ? 'horizontal' : 'vertical';
+ }
+
+ /**
+ * Handler for container mouse enter event.
+ */
+ containerMouseEnter = (): void => {
+ this.containerHovering = true;
+ if (!ScrollbarComponent.anyIsDragging) {
+ this.enabled = true;
+ this.visible = true;
+ this.activateDefaultTool();
+ // handle scroll listening from the HTML canvas element. paper doesn't have a scroll event handler
+ this.project.view.element.onwheel = (event: WheelEvent) => {
+ this.onScroll(event);
+ };
+ }
+ }
+
+ /**
+ * Handler for container mouse leave event.
+ */
+ containerMouseLeave = (): void => {
+ this.containerHovering = false;
+ if (!ScrollbarComponent.anyIsDragging) {
+ this.enabled = false;
+ this.visible = false;
+ if (ScrollbarComponent._defaultScrollbar) {
+ this.resetDefaultTool();
+ // reset default scroll listening from the HTML canvas element. paper doesn't have a scroll event handler
+ this.project.view.element.onwheel = (event: WheelEvent) => {
+ ScrollbarComponent._defaultScrollbar!.onScroll(event);
+ };
+ }
+ }
}
/**
* Activate the default tool. Used when the scrollable container is hovered or active.
*/
activateDefaultTool(): void {
- if (this.isEnabled) {
+ if (this.enabled) {
this.arrowKeyDown.activate();
}
}
/**
* Handler for the wheel event.
- * PaperJS does not have a scroll event handler, so this is set up externally where the HTML canvas element can be
- * accessed.
- * @param event WheelEvent passed from the HTML canvas.
- *
- * @example
- * const scrollbar = new ScrollbarComponent(...);
- * const canvas = this.demo.canvas.nativeElement;
- * canvas.onwheel = (event: WheelEvent) => {
- * scrollbar.onScroll(event);
- * };
+ * @param event PaperJS does not have a scroll event handler, so this is the WheelEvent passed from the HTML canvas.
*/
onScroll(event: WheelEvent): void {
- if (this.isEnabled) {
- const validScrollDirection = this.isHorizontal ? event.deltaX !== 0 : event.deltaY !== 0;
- if (!validScrollDirection) {
- return;
+ if (this.enabled) {
+ const scrollDirection = Math.abs(event.deltaX) > Math.abs(event.deltaY) ? 'horizontal' : 'vertical';
+ if (scrollDirection === this.direction) {
+ event.preventDefault();
+ this.setActiveContinuously();
+ this.isHorizontal
+ ? this.changeScrollAndContentPosition(this.scrollbar.position.x + event.deltaX)
+ : this.changeScrollAndContentPosition(this.scrollbar.position.y + event.deltaY);
+ } else if (scrollDirection === ScrollbarComponent.defaultScrollbarDirection) {
+ ScrollbarComponent._defaultScrollbar!.onScroll(event);
}
- event.preventDefault();
- this.setActiveContinuously();
- this.isHorizontal
- ? this.changeScrollAndContentPosition(this.scrollbar.position.x + event.deltaX)
- : this.changeScrollAndContentPosition(this.scrollbar.position.y + event.deltaY);
}
}
@@ -287,30 +351,41 @@ export class ScrollbarComponent extends paper.Group {
}
/**
- * Enable scrollbar visibility and interactivity.
+ * Reset current paper tool to the default scrollbar's default paper tool when another scrollbar is no longer
+ * active and hovered.
+ */
+ private resetDefaultTool() {
+ if (ScrollbarComponent._defaultScrollbar) {
+ ScrollbarComponent._defaultScrollbar!.activateDefaultTool();
+ }
+ }
+
+ /**
+ * Enable scrollbar component elements and interactivity.
*/
private enable(): void {
- this.isEnabled = true;
- this.addChildren([this.track, this.scrollbar, this.extendedTrackArea]);
+ this.enabled = true;
+ this.addChildren([this.track, this.extendedScrollbarArea, this.extendedTrackArea]);
}
/**
- * Disable scrollbar visibility and interactivity.
+ * Disable scrollbar component elements and interactivity.
*/
private disable(): void {
- this.isEnabled = false;
+ this.enabled = false;
this.removeChildren();
}
/**
- * Assign scrollable defaults for the optional properties.
+ * Assign scrollable defaults for the optional properties, so that none will be undefined.
*/
private assignScrollableDefaults(): Scrollable {
- return Object.assign({
+ return {
contentOffsetStart: DEFAULT_SCROLLABLE_OFFSET,
contentOffsetEnd: DEFAULT_SCROLLABLE_OFFSET,
- checkFitWithOffsets: true
- }, this.scrollable);
+ checkFitWithOffsets: true,
+ ...this.scrollable
+ };
}
/**
@@ -324,10 +399,10 @@ export class ScrollbarComponent extends paper.Group {
const contentWithOffset = new paper.Path.Rectangle({ rectangle: content.bounds });
if (this.isHorizontal) {
contentWithOffset.bounds.width = this.contentSizeWithOffsets();
- contentWithOffset.position.x -= (contentOffsetStart || DEFAULT_SCROLLABLE_OFFSET);
+ contentWithOffset.position.x -= contentOffsetStart!;
} else {
contentWithOffset.bounds.height = this.contentSizeWithOffsets();
- contentWithOffset.position.y -= (contentOffsetStart || DEFAULT_SCROLLABLE_OFFSET);
+ contentWithOffset.position.y -= contentOffsetStart!;
}
return contentWithOffset.isInside(containerBounds);
}
@@ -337,8 +412,7 @@ export class ScrollbarComponent extends paper.Group {
*/
private contentSizeWithOffsets(): number {
const { content, contentOffsetStart, contentOffsetEnd } = this.scrollable;
- return (this.isHorizontal ? content.bounds.width : content.bounds.height)
- + (contentOffsetStart || DEFAULT_SCROLLABLE_OFFSET) + (contentOffsetEnd || DEFAULT_SCROLLABLE_OFFSET);
+ return (this.isHorizontal ? content.bounds.width : content.bounds.height) + contentOffsetStart! + contentOffsetEnd!;
}
/**
@@ -385,7 +459,9 @@ export class ScrollbarComponent extends paper.Group {
* The proportionate length for the scrollbar. Based on viewable content size divided by the full content size.
*/
private getProportionalLength(): number {
- return (this.containerSize / this.contentSizeWithOffsets()) * this.scrollTrackLength;
+ const fullSize = this.contentSizeWithOffsets() + this.scrollable.contentOffsetEnd!
+ - this.scrollable.contentOffsetStart!;
+ return this.containerSize / fullSize * this.scrollTrackLength;
}
/**
@@ -418,7 +494,7 @@ export class ScrollbarComponent extends paper.Group {
* @param event {paper.MouseEvent}
*/
private mouseLeave(event: paper.MouseEvent): void {
- this.hovering = false;
+ this.scrollbarHovering = false;
if (!this.dragging) {
this.project.view.element.style.cursor = 'default';
this.setNormal();
@@ -429,7 +505,7 @@ export class ScrollbarComponent extends paper.Group {
* Handler for mouse enter event.
*/
private mouseEnter(): void {
- this.hovering = true;
+ this.scrollbarHovering = true;
if (!this.dragging) {
this.project.view.element.style.cursor = 'pointer';
}
@@ -509,10 +585,9 @@ export class ScrollbarComponent extends paper.Group {
private changeContentPosition(): void {
const { content, contentOffsetStart, contentOffsetEnd } = this.scrollable;
const contentMaxPosition = this.containerSize - (content.bounds[this.dimension] as number);
- const contentDistance = contentMaxPosition - (contentOffsetStart || DEFAULT_SCROLLABLE_OFFSET)
- - (contentOffsetEnd || DEFAULT_SCROLLABLE_OFFSET) * 2;
+ const contentDistance = contentMaxPosition - contentOffsetStart! - contentOffsetEnd! * 2;
(content.position[this.axis] as number) = (this.scrollAmount * contentDistance) + this.contentInitialPosition
- + (contentOffsetStart || DEFAULT_SCROLLABLE_OFFSET);
+ + contentOffsetStart!;
}
/**
@@ -523,21 +598,31 @@ export class ScrollbarComponent extends paper.Group {
let offsetPoint: paper.Point;
tool.onMouseDown = (event) => {
this.dragging = true;
+ ScrollbarComponent.anyIsDragging = true;
this.project.view.element.style.cursor = 'grabbing';
offsetPoint = new paper.Point(event.downPoint.subtract(this.scrollbar.position));
};
- tool.onMouseUp = () => {
+ tool.onMouseUp = (event: paper.ToolEvent) => {
this.dragging = false;
- this.project.view.element.style.cursor = this.hovering ? 'pointer' : 'default';
+ ScrollbarComponent.anyIsDragging = false;
+ this.project.view.element.style.cursor = this.scrollbarHovering ? 'pointer' : 'default';
+ if (!this.containerHovering) {
+ this.visible = false;
+ this.resetDefaultTool();
+ this.setNormal();
+ } else if (!this.scrollbarHovering) {
+ this.setNormal();
+ }
};
tool.onMouseDrag = (event: paper.ToolEvent) => {
this.setActive();
+ this.visible = true;
return this.isHorizontal
? this.changeScrollAndContentPosition(event.point.x - offsetPoint.x)
: this.changeScrollAndContentPosition(event.point.y - offsetPoint.y);
};
- // arrowKeyDown tool is inactive while hovering on the scrollbar and scrollbarDrag is active. this handles keyDown
- // events
+ // arrowKeyDown tool is inactive while hovering on the scrollbar and scrollbarDrag tool is active. this handles
+ // keyDown events
tool.onKeyDown = (event) => {
this.moveByKeyDown(event);
this.activateDefaultTool();
@@ -561,11 +646,12 @@ export class ScrollbarComponent extends paper.Group {
* @param event {paper.KeyEvent}
*/
private moveByKeyDown(event: paper.KeyEvent) {
- const validKeyPress = this.isHorizontal
- ? event.key === 'left' || event.key === 'right'
- : event.key === 'up' || event.key === 'down';
- if (validKeyPress) {
- const movementAmount = Math.floor(this.getProportionalLength());
+ const horizontalKeys = event.key === 'left' || event.key === 'right';
+ const verticalKeys = event.key === 'up' || event.key === 'down';
+ const keyPressDirection = (horizontalKeys && 'horizontal') || (verticalKeys && 'vertical');
+
+ if (keyPressDirection === this.direction) {
+ const movementAmount = Math.floor(this.getProportionalLength()) / 3;
event.preventDefault();
this.setActiveContinuously();
switch (event.key) {
@@ -582,6 +668,8 @@ export class ScrollbarComponent extends paper.Group {
this.changeScrollAndContentPosition(this.scrollbar.position.x + movementAmount);
break;
}
+ } else if (keyPressDirection === ScrollbarComponent.defaultScrollbarDirection) {
+ ScrollbarComponent._defaultScrollbar!.moveByKeyDown(event);
}
}
diff --git a/src/components/vapp-edge-label.test.ts b/src/components/vapp-edge-label.test.ts
new file mode 100644
index 0000000..446227c
--- /dev/null
+++ b/src/components/vapp-edge-label.test.ts
@@ -0,0 +1,21 @@
+import { VappEdgeLabelComponent } from './vapp-edge-label';
+import * as paper from 'paper';
+
+describe('vapp edge label component', () => {
+
+ beforeAll(() => {
+ const canvasEl = document.createElement('canvas');
+ paper.setup(canvasEl);
+ paper.settings.applyMatrix = false;
+ });
+
+ test('basic properties', () => {
+ const text = 'test name';
+ const position = new paper.Point(35, 80);
+ const label = new VappEdgeLabelComponent(text, position);
+ expect(label.text).toBe(text);
+ expect(label.position.x).toBe(position.x);
+ expect(label.position.y).toBe(position.y);
+ });
+
+});
diff --git a/src/components/vapp-edge-label.ts b/src/components/vapp-edge-label.ts
new file mode 100644
index 0000000..369394e
--- /dev/null
+++ b/src/components/vapp-edge-label.ts
@@ -0,0 +1,56 @@
+import * as paper from 'paper';
+import { EntityLabelComponent } from './entity-label';
+import { VAPP_BACKGROUND_COLOR, LIGHT_GREY } from '../constants/colors';
+import { CONNECTOR_RADIUS, DEFAULT_STROKE_WIDTH, LABEL_HEIGHT, CONNECTOR_MARGIN } from '../constants/dimensions';
+import { DEFAULT_STROKE_STYLE } from '../constants/styles';
+
+const ICON_COLOR = '#EBB86C';
+
+/**
+ * Vapp Nat-Routed Edge Label Visual Component.
+ */
+export class VappEdgeLabelComponent extends paper.Group {
+
+ readonly _label: EntityLabelComponent;
+
+ /**
+ * Creates a new VappEdgeLabelComponent instance.
+ *
+ * @param _text the name of the vApp edge's parent network
+ * @param _point the location that the component will be rendered at
+ */
+ constructor(private _text: string,
+ private _point: paper.Point = new paper.Point(0, 0)) {
+ super();
+ this.pivot = new paper.Point(0, 0);
+ this.position = _point;
+
+ this._label = new EntityLabelComponent(this._text, ICON_COLOR);
+ this._label.background.style = {
+ ...DEFAULT_STROKE_STYLE,
+ fillColor: VAPP_BACKGROUND_COLOR
+ };
+ this.addChild(this._label);
+
+ // create the top and bottom arcs that connects the label to the network path
+ const connectorTopArc = new paper.Path.Arc({
+ from: new paper.Point(-CONNECTOR_RADIUS, 0),
+ through: new paper.Point(0, -CONNECTOR_RADIUS),
+ to: new paper.Point(CONNECTOR_RADIUS, 0),
+ pivot: new paper.Point(0, 0),
+ position: new paper.Point(CONNECTOR_MARGIN + CONNECTOR_RADIUS - DEFAULT_STROKE_WIDTH / 2, 0),
+ fillColor: LIGHT_GREY,
+ parent: this
+ });
+ const connectorBottomArc = connectorTopArc.clone();
+ connectorBottomArc.rotate(180);
+ connectorBottomArc.position.y = LABEL_HEIGHT;
+ }
+
+ /**
+ * Gets the label text.
+ */
+ get text(): string {
+ return this._text;
+ }
+}
diff --git a/src/components/vapp-network-list.test.ts b/src/components/vapp-network-list.test.ts
new file mode 100644
index 0000000..c3f14de
--- /dev/null
+++ b/src/components/vapp-network-list.test.ts
@@ -0,0 +1,62 @@
+import { VappNetworkListComponent } from './vapp-network-list';
+import { VappNetworkData } from './vapp-network';
+import * as paper from 'paper';
+
+describe('vapp component', () => {
+
+ beforeAll(() => {
+ const canvasEl = document.createElement('canvas');
+ paper.setup(canvasEl);
+ paper.settings.applyMatrix = false;
+ });
+
+ test('basic properties', () => {
+ const vappNetworksData: VappNetworkData[] = [
+ {
+ uuid: '0',
+ name: '172.16.55.0 Failover Network 1',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ },
+ {
+ uuid: '1',
+ name: '172.16.55.0 Failover Network 2',
+ vapp_uuid: '',
+ fence_mode: 'NAT_ROUTED'
+ },
+ {
+ uuid: '2',
+ name: '172.16.55.0 Failover Network 3',
+ vapp_uuid: '',
+ fence_mode: 'ISOLATED'
+ }
+ ];
+ const position = new paper.Point(0, 0);
+ const networks = new VappNetworkListComponent(vappNetworksData);
+ vappNetworksData.forEach((data, i) => {
+ expect(networks.data[i].uuid).toBe(data.uuid);
+ expect(networks.data[i].name).toBe(data.name);
+ expect(networks.data[i].vapp_uuid).toBe(data.vapp_uuid);
+ expect(networks.data[i].fence_mode).toBe(data.fence_mode);
+ });
+ expect(networks.position.x).toBe(position.x);
+ expect(networks.position.y).toBe(position.y);
+ expect(networks.children.length).toBe(3); // 3 paths created from data
+ });
+
+ test('with no networks', () => {
+ const vappNetworksData: VappNetworkData[] = [];
+ const position = new paper.Point(30, 40);
+ const networks = new VappNetworkListComponent(vappNetworksData, position);
+ vappNetworksData.forEach((data, i) => {
+ expect(networks.data[i].uuid).toBe(data.uuid);
+ expect(networks.data[i].name).toBe(data.name);
+ expect(networks.data[i].vapp_uuid).toBe(data.vapp_uuid);
+ expect(networks.data[i].fence_mode).toBe(data.fence_mode);
+ });
+ expect(networks.position.x).toBe(position.x);
+ expect(networks.position.y).toBe(position.y);
+ expect(networks.children.length).toBe(0); // no paths
+ });
+
+});
diff --git a/src/components/vapp-network-list.ts b/src/components/vapp-network-list.ts
new file mode 100644
index 0000000..c762e88
--- /dev/null
+++ b/src/components/vapp-network-list.ts
@@ -0,0 +1,113 @@
+import * as paper from 'paper';
+import { VappNetworkData, VappNetworkComponent } from './vapp-network';
+import { LowestVnicPointByNetworkName } from './vm-and-vnic-list';
+import { VAPP_NETWORK_RIGHT_MARGIN } from '../constants/dimensions';
+
+/**
+ * Interface for network positions by network name.
+ */
+export interface VappNetworkPositionsByName {
+ [name: string]: paper.Point;
+}
+
+/**
+ * Virtual Application Network Visual Component.
+ */
+export class VappNetworkListComponent extends paper.Group {
+ // store network position by network name for matching vnic horizontal positioning
+ private _networkPositionsByName: VappNetworkPositionsByName = {};
+ // list of network paths for easier iteration
+ private networkPathList: VappNetworkComponent[] = [];
+ // number of isolated networks used to stagger the height of isolated network labels
+ private isolatedNetworkCount = 0;
+
+ /**
+ * Creates a new VappNetworkListComponent instance.
+ *
+ * @param vappNetworks the vapp networks data
+ * @param _point the location that the vapp network list should be rendered at
+ */
+ constructor(private _vappNetworks: VappNetworkData[],
+ private _point: paper.Point = new paper.Point(0, 0)) {
+ super();
+ this.pivot = new paper.Point(0, 0);
+ this.position = this._point;
+
+ // create and start the network path at the vapp's top boundary
+ // number of edge networks used to stagger the height of edge network labels
+ let edgeNetworkCount = 0;
+ // TODO: pass in the y coordinate of matching org-vdc network when they are eventually included
+ this._vappNetworks.forEach((networkData, index) => {
+ const network = new VappNetworkComponent(
+ networkData,
+ new paper.Point(VAPP_NETWORK_RIGHT_MARGIN * index, 0),
+ edgeNetworkCount
+ );
+ this._networkPositionsByName[networkData.name] = network.position;
+ this.networkPathList.push(network);
+ // insert at the bottom below any existing vapp network edge labels that were added in the VappNetworkComponent
+ this.insertChild(0, network);
+ if (networkData.fence_mode === 'NAT_ROUTED') {
+ edgeNetworkCount++;
+ } else if (networkData.fence_mode === 'ISOLATED') {
+ this.isolatedNetworkCount++;
+ }
+ });
+ }
+
+ /**
+ * Gets the position of the last vApp network (furthest right).
+ */
+ get lastNetworkPosition(): paper.Point {
+ const listCount = this.networkPathList.length;
+ return listCount ? this.networkPathList[listCount - 1].position : new paper.Point(0, 0);
+ }
+
+ /**
+ * Gets the vApp Network data.
+ */
+ get data(): VappNetworkData[] {
+ return this._vappNetworks;
+ }
+
+ /**
+ * Gets vApp network positions by name data for horizontally position matching VNICs.
+ */
+ get networkPositionsByName(): VappNetworkPositionsByName {
+ return this._networkPositionsByName;
+ }
+
+ /**
+ * Sets the vapp network top segment (above the vapp background) and the bottom segment (inside the vapp).
+ * @param lowestVnicPointByNetworkName lowest vnic position by network name from VmAndVnicListComponent
+ */
+ setTopAndBottomSegments(lowestVnicPointByNetworkName: LowestVnicPointByNetworkName) {
+ this.networkPathList.forEach(network => {
+ // top segment above vApp background
+ network.setTopmostSegmentAndConnection(this.isolatedNetworkCount);
+ // bottom segment within vApp drawn to the lowest attached VNIC or disconnected if it has no VNICs
+ if (lowestVnicPointByNetworkName[network.data.name]) {
+ network.setBottommostSegment(lowestVnicPointByNetworkName[network.data.name].y);
+ } else {
+ network.setDisconnected();
+ }
+ if (network.data.fence_mode === 'ISOLATED') {
+ this.isolatedNetworkCount--;
+ }
+ });
+ }
+
+ /**
+ * Clones the bottom segment of vapp networks to separate for scrolling and removes the original segment.
+ * @param splitPositionY vertical point where the network path should be split for cloning and separation
+ */
+ cloneVmListSegments(splitPositionY: number): paper.Group {
+ const clones = new paper.Group();
+ clones.pivot = new paper.Point(0, 0);
+ clones.position = this._point;
+ this.networkPathList.forEach(network => {
+ clones.addChild(network.cloneAndSplit(splitPositionY));
+ });
+ return clones;
+ }
+}
diff --git a/src/components/vapp-network.test.ts b/src/components/vapp-network.test.ts
new file mode 100644
index 0000000..1c5c104
--- /dev/null
+++ b/src/components/vapp-network.test.ts
@@ -0,0 +1,111 @@
+import { VappNetworkComponent, VappNetworkData } from './vapp-network';
+import * as paper from 'paper';
+import { CONNECTOR_RADIUS, VAPP_PADDING, LABEL_HEIGHT } from '../constants/dimensions';
+
+describe('vapp component', () => {
+
+ beforeAll(() => {
+ const canvasEl = document.createElement('canvas');
+ paper.setup(canvasEl);
+ paper.settings.applyMatrix = false;
+ });
+
+ test('basic properties', () => {
+ const vappNetworkData: VappNetworkData = {
+ uuid: '0',
+ name: 'A',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ };
+ const position = new paper.Point(0, 0);
+ const network = new VappNetworkComponent(vappNetworkData);
+ expect(network.data.uuid).toBe(vappNetworkData.uuid);
+ expect(network.data.name).toBe(vappNetworkData.name);
+ expect(network.data.vapp_uuid).toBe(vappNetworkData.vapp_uuid);
+ expect(network.data.fence_mode).toBe(vappNetworkData.fence_mode);
+ expect(network.position.x).toBe(position.x);
+ expect(network.position.y).toBe(position.y);
+ expect(network.children.length).toBe(1); // path only
+ });
+
+ test('nat-routed topmost segment', () => {
+ const vappNetworkData: VappNetworkData = {
+ uuid: '0',
+ name: 'A',
+ vapp_uuid: '',
+ fence_mode: 'NAT_ROUTED'
+ };
+ const edgeCount = 0;
+ const topmostPointY = 20;
+ const position = new paper.Point(40, 50);
+ const isolatedCount = 0;
+ const network = new VappNetworkComponent(vappNetworkData, position, edgeCount, topmostPointY);
+ network.setTopmostSegmentAndConnection(isolatedCount);
+ expect(network.children.length).toBe(3); // path, edge label, and connection
+ expect(network.path.bounds.top).toBe(-(topmostPointY + CONNECTOR_RADIUS));
+ });
+
+ test('isolated topmost segment', () => {
+ const vappNetworkData: VappNetworkData = {
+ uuid: '0',
+ name: 'A',
+ vapp_uuid: '',
+ fence_mode: 'ISOLATED'
+ };
+ const position = new paper.Point(0, 0);
+ const isolatedCount = 1;
+ const network = new VappNetworkComponent(vappNetworkData, position);
+ network.setTopmostSegmentAndConnection(isolatedCount);
+ expect(network.children.length).toBe(2); // path and connection
+ // (CONNECTOR_RADIUS + MULTIPLE_ISOLATED_NETWORK_PADDING) * isolatedCount + ISOLATED_NETWORK_PADDING ~ 11/2 + 13 + 5
+ expect(network.path.bounds.top).toBe(-23.5);
+ });
+
+ test('setBottommostSegment method', () => {
+ const vappNetworkData: VappNetworkData = {
+ uuid: '0',
+ name: 'A',
+ vapp_uuid: '',
+ fence_mode: 'ISOLATED'
+ };
+ const pointY = 20;
+ const network = new VappNetworkComponent(vappNetworkData);
+ network.setBottommostSegment(pointY);
+ expect(network.children.length).toBe(1); // path only
+ expect(network.path.bounds.bottom).toBe(pointY);
+ });
+
+ test('setDisconnected method', () => {
+ const vappNetworkData: VappNetworkData = {
+ uuid: '0',
+ name: 'A',
+ vapp_uuid: '',
+ fence_mode: 'ISOLATED'
+ };
+ const network = new VappNetworkComponent(vappNetworkData);
+ network.setDisconnected();
+ expect(network.children.length).toBe(2); // path and bullet point connection icon component
+ expect(network.path.bounds.bottom).toBe(VAPP_PADDING + LABEL_HEIGHT / 2);
+ });
+
+ test('cloneAndSplit method', () => {
+ const vappNetworkData: VappNetworkData = {
+ uuid: '0',
+ name: 'A',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ };
+ const position = new paper.Point(85, 10);
+ const network = new VappNetworkComponent(vappNetworkData, position);
+ const length = 100;
+ const split = 50;
+ network.setTopmostSegmentAndConnection(0);
+ network.setBottommostSegment(length);
+ const initialSegmentCount = network.path.segments.length;
+ const clone = network.cloneAndSplit(split);
+ expect(network.path.segments.length).toBe(initialSegmentCount); // a segment was added and a segment was removed
+ expect(clone.length).toBe(length - split);
+ expect(clone.position.x).toBe(network.path.position.x + position.x);
+ });
+
+});
diff --git a/src/components/vapp-network.ts b/src/components/vapp-network.ts
new file mode 100644
index 0000000..9d058ef
--- /dev/null
+++ b/src/components/vapp-network.ts
@@ -0,0 +1,160 @@
+import * as paper from 'paper';
+import { CANVAS_BACKGROUND_COLOR } from '../constants/colors';
+import { IsolatedNetworkLabelComponent } from './isolated-network-label';
+import { VappEdgeLabelComponent } from './vapp-edge-label';
+import { ConnectionIconComponent } from './connection-icon';
+import { BulletPointConnectionIconComponent } from './bullet-point-connection-icon';
+import { CONNECTOR_RADIUS, CONNECTOR_MARGIN, VAPP_PADDING, LABEL_HEIGHT, DEFAULT_STROKE_WIDTH, LABEL_BOTTOM_PADDING }
+ from '../constants/dimensions';
+import { DEFAULT_STROKE_STYLE } from '../constants/styles';
+
+const MULTIPLE_ISOLATED_NETWORK_PADDING = 13;
+const ISOLATED_NETWORK_PADDING = 5;
+
+/**
+ * Enumeration of vApp fence modes.
+ */
+export type FenceMode = 'BRIDGED' | 'NAT_ROUTED' | 'ISOLATED';
+
+/**
+ * Interface for vApp network data.
+ */
+export interface VappNetworkData {
+ uuid: string;
+ name: string;
+ vapp_uuid: string;
+ fence_mode: FenceMode;
+}
+
+/**
+ * Virtual Application Network Visual Component.
+ */
+export class VappNetworkComponent extends paper.Group {
+
+ readonly _path: paper.Path.Line;
+ // connection icon or isolated network label at the top of the network path
+ private connectionComponent: ConnectionIconComponent | IsolatedNetworkLabelComponent;
+ readonly edgeLabel: VappEdgeLabelComponent;
+ readonly isNatRouted: boolean = false;
+ readonly isIsolated: boolean = false;
+ // network has no attached vnics
+ private isDisconnected: boolean = false;
+
+ /**
+ * Creates a new VappNetworkComponent instance.
+ *
+ * @param _vappNetwork the vapp network data
+ * @param _point the location that the vapp network should be rendered at
+ * @param edgeNetworkCount the number of nat-routed networks
+ * @param topmostPointY the y coordinate of the matching org-vdc network it will be connected to
+ */
+ // TODO: 59 is hardcoded to topmostPointY in for the vapp demo. replace it with the matching org-vdc vertical position
+ // when it's eventually added
+ constructor(private _vappNetwork: VappNetworkData,
+ private _point: paper.Point = new paper.Point(0, 0),
+ private edgeNetworkCount: number = 0,
+ private topmostPointY: number = 59) {
+ super();
+ this.pivot = new paper.Point(0, 0);
+ this.position = this._point;
+
+ this.isNatRouted = this._vappNetwork.fence_mode === 'NAT_ROUTED';
+ this.isIsolated = this._vappNetwork.fence_mode === 'ISOLATED';
+
+ // create and start the network path at the vApp top boundary
+ this._path = new paper.Path.Line({
+ segment: new paper.Point(0, 0),
+ style: DEFAULT_STROKE_STYLE,
+ parent: this
+ });
+
+ // add vapp edge label if needed
+ if (this.isNatRouted) {
+ const pointX = -CONNECTOR_MARGIN - CONNECTOR_RADIUS;
+ const pointY = (LABEL_HEIGHT + LABEL_BOTTOM_PADDING + DEFAULT_STROKE_WIDTH) * edgeNetworkCount
+ + VAPP_PADDING + LABEL_HEIGHT + LABEL_BOTTOM_PADDING;
+ this.edgeLabel = new VappEdgeLabelComponent(this._vappNetwork.name, new paper.Point(pointX, pointY));
+ this.addChild(this.edgeLabel);
+ }
+ }
+
+ /**
+ * Gets the network path.
+ */
+ get path(): paper.Path.Line {
+ return this._path;
+ }
+
+ /**
+ * Gets the VAppNetworkData.
+ */
+ get data(): VappNetworkData {
+ return this._vappNetwork;
+ }
+
+ /**
+ * Sets the topmost segment outside of the vApp and connection icon.
+ * @param isolatedCount the number of isolated networks used to determine the topmost point.
+ */
+ setTopmostSegmentAndConnection(isolatedCount: number): void {
+ // create the topmost point
+ const topmostPositionY = this.isIsolated
+ ? (CONNECTOR_RADIUS + MULTIPLE_ISOLATED_NETWORK_PADDING) * isolatedCount + ISOLATED_NETWORK_PADDING
+ : this.topmostPointY + CONNECTOR_RADIUS;
+ const topmostPoint = new paper.Point(0, -topmostPositionY);
+ // add the topmost point to the path
+ this.path.add(topmostPoint);
+ // add the connection component based on network's fence mode
+ this.connectionComponent = this.isIsolated
+ ? new IsolatedNetworkLabelComponent(this._vappNetwork.name, topmostPoint)
+ : new ConnectionIconComponent(topmostPoint, CANVAS_BACKGROUND_COLOR);
+ this.addChild(this.connectionComponent);
+ }
+
+ /**
+ * Sets the bottommost segment if the vApp has at least one attached VNIC.
+ * @param pointY the y coordinate of the vApp network's lowest attached VNIC.
+ */
+ setBottommostSegment(pointY: number): void {
+ this._path.add(new paper.Point(0, pointY));
+ }
+
+ /**
+ * Sets the bottommost segment and connection icon if the vApp has no attached VNICs.
+ */
+ setDisconnected(): void {
+ this.isDisconnected = true;
+ if (!this.isNatRouted) {
+ // add the bottommost point and disconnected icon next to the vapp label
+ this._path.add(new paper.Point(0, VAPP_PADDING + LABEL_HEIGHT / 2));
+ this.addChild(new BulletPointConnectionIconComponent(this._path.bounds.bottomCenter));
+ } else {
+ // add the bottommost point at the edge label's position
+ this.path.add(new paper.Point(0, this.edgeLabel.position.y));
+ }
+ }
+
+ /**
+ * Clones and splits the bottom segment in the VmAndVnicListComponent if scrolling is required.
+ * @param splitPositionY vertical position where the network path should be split for cloning and separation
+ */
+ cloneAndSplit(splitPositionY: number): paper.Path {
+ let clone: paper.Path = new paper.Path();
+ // disconnected networks (without any attached vnics) would not have a segment in the vm and vnic list component
+ if (!this.isDisconnected) {
+ // add new point to split the path at the top of the vm list
+ this._path.add(new paper.Point(0, splitPositionY));
+ const segments = this._path.segments;
+ // clone last segment
+ clone = new paper.Path.Line({
+ from: segments[3].point,
+ to: segments[4].point,
+ style: DEFAULT_STROKE_STYLE
+ });
+ clone.position.x = this.position.x;
+ // remove the original reference segment
+ segments[3].remove();
+ }
+ return clone;
+ }
+}
diff --git a/src/components/vapp.test.ts b/src/components/vapp.test.ts
new file mode 100644
index 0000000..f8dd87a
--- /dev/null
+++ b/src/components/vapp.test.ts
@@ -0,0 +1,81 @@
+import { VappComponent, VappData } from './vapp';
+import * as paper from 'paper';
+
+describe('vapp component', () => {
+
+ beforeAll(() => {
+ const xhrMockClass = () => ({
+ open : jest.fn(),
+ send : jest.fn(),
+ setRequestHeader: jest.fn()
+ });
+ (window as any).XMLHttpRequest = jest.fn().mockImplementation(xhrMockClass);
+ const canvasEl = document.createElement('canvas');
+ paper.setup(canvasEl);
+ });
+
+ test('basic properties', () => {
+ const vappData: VappData = {
+ uuid: '',
+ name: 'Coke RES & BURST',
+ vapp_networks: [
+ {
+ uuid: '0',
+ name: 'A',
+ vapp_uuid: '',
+ fence_mode: 'BRIDGED'
+ }
+ ],
+ vms: [
+ {
+ uuid: '',
+ name: 'Alert Resource Non Regression VM',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'AutomatedSecurityTest1',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'CatalogResourceNonRegression1',
+ vapp_uuid: '',
+ operatingSystem: 'ubuntu64Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: true
+ }
+ ]
+ }
+ ]
+ };
+ const position = new paper.Point(20, 15);
+ const vapp = new VappComponent(vappData, position);
+ expect(vapp.data.uuid).toBe(vappData.uuid);
+ expect(vapp.data.name).toBe(vappData.name);
+ expect(vapp.data.vapp_networks).toBe(vappData.vapp_networks);
+ expect(vapp.data.vms).toBe(vappData.vms);
+ expect(vapp.position.x).toBe(position.x);
+ expect(vapp.position.y).toBe(position.y);
+ });
+
+});
diff --git a/src/components/vapp.ts b/src/components/vapp.ts
new file mode 100644
index 0000000..b05ea02
--- /dev/null
+++ b/src/components/vapp.ts
@@ -0,0 +1,221 @@
+import * as paper from 'paper';
+import { EntityLabelComponent } from './entity-label';
+import { VmData } from './vm';
+import { VmAndVnicListComponent } from './vm-and-vnic-list';
+import { VAPP_BACKGROUND_COLOR } from '../constants/colors';
+import { CONNECTOR_RADIUS, VIEW_BOTTOM_PADDING, DEFAULT_SCROLLBAR_THICKNESS, VAPP_NETWORK_RIGHT_MARGIN, VAPP_PADDING,
+ DEFAULT_STROKE_WIDTH } from '../constants/dimensions';
+import { MarginComponent } from './margin';
+import { VappNetworkData } from './vapp-network';
+import { VappNetworkListComponent } from './vapp-network-list';
+import { ScrollbarComponent } from './scrollbar';
+
+const MARGIN_RIGHT = 30;
+const BACKGROUND_RADIUS = 5;
+const LABEL_ICON_COLOR = '#CA67B8';
+const VAPP_LABEL_BOTTOM_MARGIN = 20;
+const EDGE_LABEL_BOTTOM_MARGIN = 15;
+
+/**
+ * Interface for vApp data.
+ */
+export interface VappData {
+ uuid: string;
+ name: string;
+ vapp_networks: VappNetworkData[];
+ vms: VmData[];
+}
+
+/**
+ * Virtual Application Visual Component.
+ */
+export class VappComponent extends paper.Group {
+
+ private label: EntityLabelComponent;
+ private background: paper.Path.Rectangle;
+ private _margin: MarginComponent;
+ private vms: VmAndVnicListComponent;
+ private vappNetworks: VappNetworkListComponent;
+ // position for the division between any labels and vm/vnic list
+ private divisionPositionY: number;
+ private scrollbar: ScrollbarComponent;
+ // content needs scrollbar
+ private _isScrollable: boolean = false;
+
+ /**
+ * Creates a new VappComponent instance.
+ *
+ * @param _vapp the vapp data
+ * @param _point the location that the vm should be rendered at
+ */
+ constructor(private _vapp: VappData,
+ private _point: paper.Point = new paper.Point(0, 0)) {
+ super();
+ this.pivot = new paper.Point(0, 0);
+ this.position = _point;
+
+ let backgroundOffsetX = 0;
+ let backgroundOffsetY = 0;
+ const vappNetworkCount = this._vapp.vapp_networks.length;
+ const vmCount = this._vapp.vms.length;
+
+ // vapp label
+ // x position based on if there are any vApp networks (and how many) or vms
+ const labelPositionX = vappNetworkCount || vmCount
+ ? Math.max(1, vappNetworkCount) * VAPP_NETWORK_RIGHT_MARGIN + VAPP_PADDING
+ : VAPP_PADDING;
+ this.label = new EntityLabelComponent(
+ this._vapp.name,
+ LABEL_ICON_COLOR,
+ new paper.Point(labelPositionX, VAPP_PADDING),
+ { fontWeight: 'bold' }
+ );
+ this.addChild(this.label);
+
+ // start vapp network paths - add a point to each network to have x position data for matching vnics
+ // and edge labels to have y position data for vapp header (which includes the vapp label and edge labels)
+ if (vappNetworkCount) {
+ this.vappNetworks = new VappNetworkListComponent(
+ this._vapp.vapp_networks,
+ new paper.Point(VAPP_PADDING + CONNECTOR_RADIUS, 0));
+ this.addChild(this.vappNetworks);
+ }
+
+ // calculate division position between the vapp header (which includes the vapp label and edge labels) and vm
+ // and vnic list
+ const headerBase = this.globalToLocal(this.bounds.bottomLeft).y;
+ // headerBase will equal vapp label bottom if there are no edge labels. margin based on which label is positionally
+ // the lowest.
+ const vappLabelIsBottomMost = headerBase === this.label.bounds.bottom;
+ const headerBaseMargin = vappLabelIsBottomMost ? VAPP_LABEL_BOTTOM_MARGIN : EDGE_LABEL_BOTTOM_MARGIN;
+ this.divisionPositionY = headerBase + headerBaseMargin;
+
+ // maximum bottom position for vApp background based on the canvas view size
+ const maxBottomPosition = this.globalToLocal(paper.view.bounds.bottomLeft).y - VIEW_BOTTOM_PADDING;
+
+ // vms and vnics list
+ this.vms = new VmAndVnicListComponent(
+ this._vapp.vms,
+ this.vappNetworks && this.vappNetworks.networkPositionsByName,
+ this.vappNetworks && this.vappNetworks.lastNetworkPosition,
+ new paper.Point(VAPP_PADDING + DEFAULT_STROKE_WIDTH, this.divisionPositionY));
+ this.addChild(this.vms);
+
+ this._isScrollable = this.vms.bounds.bottom > maxBottomPosition;
+
+ // finish VappNetworks - draw the top external segment and internal segment based on matching vnic positions
+ if (vappNetworkCount) {
+ this.vappNetworks.setTopAndBottomSegments(this.vms.lowestVnicPointByNetworkName);
+ // offset based on any edge labels that extend too far left (if it's on the first left-most vapp network)
+ backgroundOffsetX = VAPP_PADDING - this.vappNetworks.bounds.left;
+ // offset based on the vapp network paths' top external segment
+ backgroundOffsetY = -this.vappNetworks.bounds.top + VAPP_PADDING;
+ }
+
+ // handles case where the vapp edge label is the bottom most child (when a nat-routed network has no attached
+ // vnics and there are no vms) and updates background offset to accommodate the label's bottom arc icon
+ if (!vmCount && !vappLabelIsBottomMost) {
+ backgroundOffsetY += CONNECTOR_RADIUS;
+ }
+
+ // background - based on the bounds of all current items and placed beneath everything
+ const content = this.bounds;
+ this.background = new paper.Path.Rectangle({
+ size: new paper.Size(
+ content.width + VAPP_PADDING * 2 - backgroundOffsetX,
+ this._isScrollable ? maxBottomPosition : content.height + VAPP_PADDING * 2 - backgroundOffsetY),
+ point: new paper.Point(0, 0),
+ radius: BACKGROUND_RADIUS,
+ fillColor: VAPP_BACKGROUND_COLOR
+ });
+ this.insertChild(0, this.background);
+
+ // set up scrolling if necessary
+ if (this._isScrollable) {
+ this.clipAndScrollVmList();
+ }
+
+ // margin - used by other vapps for static or dynamic positioning
+ this._margin = new MarginComponent(this.background, 0, MARGIN_RIGHT, 0, 0);
+ this.insertChild(0, this._margin);
+ }
+
+ /**
+ * Gets the vApp data.
+ */
+ get data(): VappData {
+ return this._vapp;
+ }
+
+ /**
+ * Gets the margin.
+ */
+ get margin(): MarginComponent {
+ return this._margin;
+ }
+
+ /**
+ * Clips and adds scrolling to the VmAndVnicList component when it's too large for the view.
+ */
+ private clipAndScrollVmList(): void {
+ // create drop shadow at the top of the vm list that fades in or out onScroll
+ const dropShadow = new paper.Path.Rectangle({
+ point: new paper.Point(0, 0),
+ size: new paper.Size(this.bounds.width, this.divisionPositionY),
+ opacity: 0,
+ style: {
+ fillColor: VAPP_BACKGROUND_COLOR,
+ shadowColor: new paper.Color(0, 0, 0, 0.25),
+ shadowBlur: 5,
+ shadowOffset: new paper.Point(0, 2)
+ }
+ });
+
+ // clip mask container that will clip any vm vnic list component items outside of the vapp background
+ const vmListClipMask = new paper.Path.Rectangle(
+ new paper.Point(0, this.divisionPositionY),
+ new paper.Point(this.background.bounds.bottomRight));
+
+ // clones segment of vapp network paths inside the vm vnic list component to separate for scrolling
+ const vappNetworkClone = this.vappNetworks.cloneVmListSegments(this.divisionPositionY);
+
+ // items that will be scrollable
+ const scrollableContent = new paper.Group({
+ children: [vappNetworkClone, this.vms]
+ });
+
+ // apply clip mask
+ // tslint:disable-next-line
+ new paper.Group({
+ children: [vmListClipMask, scrollableContent, dropShadow],
+ clipped: true,
+ parent: this
+ });
+
+ // scrollbar set up
+ const scrollbarPadding = 5;
+ this.scrollbar = new ScrollbarComponent({
+ content: scrollableContent,
+ container: this,
+ containerBounds: vmListClipMask.bounds,
+ contentOffsetEnd: VAPP_PADDING / 2
+ },
+ new paper.Point(vmListClipMask.bounds.right - DEFAULT_SCROLLBAR_THICKNESS - scrollbarPadding,
+ this.divisionPositionY),
+ vmListClipMask.bounds.height - VAPP_PADDING,
+ 'vertical'
+ );
+ this.addChild(this.scrollbar);
+ // drop shadow fades in or out onScroll
+ this.scrollbar.setCustomEffects({
+ setActive: function() {
+ dropShadow.opacity = 1;
+ },
+ setNormal: function() {
+ (dropShadow as any).tweenTo({
+ opacity: 0
+ }, 150);
+ }
+ });
+ }
+}
diff --git a/src/components/vm-and-vnic-list.test.ts b/src/components/vm-and-vnic-list.test.ts
new file mode 100644
index 0000000..6d2dd38
--- /dev/null
+++ b/src/components/vm-and-vnic-list.test.ts
@@ -0,0 +1,207 @@
+import { VmAndVnicListComponent, LowestVnicPointByNetworkName } from './vm-and-vnic-list';
+import { VappNetworkPositionsByName } from './vapp-network-list';
+import { VmData } from './vm';
+import * as paper from 'paper';
+import { CONNECTOR_RADIUS, DEFAULT_STROKE_WIDTH, LABEL_HEIGHT, VM_MARGIN_VERTICAL } from '../constants/dimensions';
+
+describe('vapp component', () => {
+
+ beforeAll(() => {
+ const xhrMockClass = () => ({
+ open : jest.fn(),
+ send : jest.fn(),
+ setRequestHeader: jest.fn()
+ });
+ (window as any).XMLHttpRequest = jest.fn().mockImplementation(xhrMockClass);
+ const canvasEl = document.createElement('canvas');
+ paper.setup(canvasEl);
+ paper.settings.applyMatrix = false;
+ });
+
+ function getExpectedVnicPoints(vmData: VmData[],
+ networkPositions: VappNetworkPositionsByName,
+ position: paper.Point): LowestVnicPointByNetworkName {
+ const expectedVnicPoints: LowestVnicPointByNetworkName = {};
+ let networkCount = 0;
+ vmData.forEach(vm => {
+ vm.vnics.forEach(vnic => {
+ if (expectedVnicPoints[vnic.network_name]) {
+ expectedVnicPoints[vnic.network_name].y += LABEL_HEIGHT + VM_MARGIN_VERTICAL;
+ } else {
+ expectedVnicPoints[vnic.network_name] =
+ new paper.Point(
+ networkPositions[vnic.network_name].x + position.x + CONNECTOR_RADIUS - DEFAULT_STROKE_WIDTH,
+ networkCount * (LABEL_HEIGHT + VM_MARGIN_VERTICAL) + LABEL_HEIGHT / 2 + position.y);
+ networkCount++;
+ }
+ });
+ });
+ return expectedVnicPoints;
+ }
+
+ test('basic properties and vnics on different networks', () => {
+ const vmsData: VmData[] = [
+ {
+ uuid: '',
+ name: 'Alert Resource Non Regression VM',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'AutomatedSecurityTest1',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'B',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'CatalogResourceNonRegression1',
+ vapp_uuid: '',
+ operatingSystem: 'ubuntu64Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'C',
+ is_connected: true
+ }
+ ]
+ }
+ ];
+ const networkPositions: VappNetworkPositionsByName = {
+ A: new paper.Point(0, 30),
+ B: new paper.Point(20, 30),
+ C: new paper.Point(40, 30)
+ };
+ const position = new paper.Point(0, 0);
+ const vms = new VmAndVnicListComponent(vmsData, networkPositions);
+ vmsData.forEach((data, i) => {
+ expect(vms.data[i].uuid).toBe(data.uuid);
+ expect(vms.data[i].name).toBe(data.name);
+ expect(vms.data[i].operatingSystem).toBe(data.operatingSystem);
+ expect(vms.data[i].vnics).toBe(data.vnics);
+ });
+ expect(vms.lowestVnicPointByNetworkName).toEqual(getExpectedVnicPoints(vmsData, networkPositions, position));
+ expect(vms.position.x).toBe(position.x);
+ expect(vms.position.y).toBe(position.y);
+ });
+
+ test('multiple vnics on same network', () => {
+ const vmsData: VmData[] = [
+ {
+ uuid: '',
+ name: 'Alert Resource Non Regression VM',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'AutomatedSecurityTest1',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'CatalogResourceNonRegression1',
+ vapp_uuid: '',
+ operatingSystem: 'ubuntu64Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: true
+ }
+ ]
+ }
+ ];
+ const networkPositions: VappNetworkPositionsByName = {
+ A: new paper.Point(0, 30)
+ };
+ const position = new paper.Point(20, 50);
+ const vms = new VmAndVnicListComponent(vmsData, networkPositions, networkPositions['A'], position);
+ expect(vms.lowestVnicPointByNetworkName).toEqual(getExpectedVnicPoints(vmsData, networkPositions, position));
+ expect(vms.position.x).toBe(position.x);
+ expect(vms.position.y).toBe(position.y);
+ });
+
+ test('mix vnics on same network or different network', () => {
+ const vmsData: VmData[] = [
+ {
+ uuid: '',
+ name: 'Alert Resource Non Regression VM',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'AutomatedSecurityTest1',
+ vapp_uuid: '',
+ operatingSystem: 'windows7Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'B',
+ is_connected: true
+ }
+ ]
+ },
+ {
+ uuid: '',
+ name: 'CatalogResourceNonRegression1',
+ vapp_uuid: '',
+ operatingSystem: 'ubuntu64Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'B',
+ is_connected: true
+ }
+ ]
+ }
+ ];
+ const networkPositions: VappNetworkPositionsByName = {
+ A: new paper.Point(0, 30),
+ B: new paper.Point(20, 30)
+ };
+ const position = new paper.Point(60, 10);
+ const vms = new VmAndVnicListComponent(vmsData, networkPositions, networkPositions['B'], position);
+ expect(vms.lowestVnicPointByNetworkName).toEqual(getExpectedVnicPoints(vmsData, networkPositions, position));
+ expect(vms.position.x).toBe(position.x);
+ expect(vms.position.y).toBe(position.y);
+ });
+
+});
diff --git a/src/components/vm-and-vnic-list.ts b/src/components/vm-and-vnic-list.ts
new file mode 100644
index 0000000..0fd8f52
--- /dev/null
+++ b/src/components/vm-and-vnic-list.ts
@@ -0,0 +1,97 @@
+import * as paper from 'paper';
+import { VmData, VmComponent } from './vm';
+import { VnicComponent } from './vnic';
+import { CONNECTOR_RADIUS, CONNECTOR_SIZE, CONNECTOR_RIGHT_MARGIN, DEFAULT_STROKE_WIDTH, LABEL_HEIGHT,
+ VM_MARGIN_VERTICAL } from '../constants/dimensions';
+import { VappNetworkPositionsByName } from './vapp-network-list';
+
+/**
+ * Interface for the lowest vnic point by network name.
+ */
+export interface LowestVnicPointByNetworkName {
+ [name: string]: paper.Point;
+}
+
+/**
+ * Vm List Visual Component.
+ */
+export class VmAndVnicListComponent extends paper.Group {
+
+ // store lowest vnic point by network name for setting the lowest point of the vapp network path
+ private _lowestVnicPointByNetworkName: LowestVnicPointByNetworkName = {};
+
+ /**
+ * Creates a new VmAndVnicListComponent instance.
+ *
+ * @param _vms the vms data
+ * @param vappNetworkPositionsByName the vapp network positions by name from the VappNetworkListComponent used for
+ * positioning x value of matching vnics
+ * @param lastNetworkPosition the position of the last (furthest right) vapp network used for positioning vms and
+ * unattached vnics
+ * @param _point the location that the vm and vnic list should be rendered at
+ */
+ constructor(private _vms: Array,
+ private vappNetworkPositionsByName: VappNetworkPositionsByName = {},
+ private lastNetworkPosition: paper.Point = new paper.Point(0, 0),
+ private _point: paper.Point = new paper.Point(0, 0)) {
+ super();
+ this.pivot = new paper.Point(0, 0);
+ this.position = _point;
+
+ // reusable values
+ const hasVappNetworks = Object.keys(this.vappNetworkPositionsByName).length !== 0;
+ // middle of vm label
+ const vnicOffsetY = LABEL_HEIGHT / 2;
+ // offset from vnic pivot in the center to the left stroke bounds
+ const vnicOffsetX = CONNECTOR_RADIUS - DEFAULT_STROKE_WIDTH;
+
+ // draw vms and their vnics
+ this._vms.forEach((vmData, index) => {
+ const sharedPointY = (LABEL_HEIGHT + VM_MARGIN_VERTICAL) * index;
+ let xPositionMultiplier = hasVappNetworks ? 1 : 0;
+
+ vmData.vnics.forEach(vnicData => {
+ const matchingNetwork = this.vappNetworkPositionsByName[vnicData.network_name];
+ const vnicPointX = matchingNetwork ? matchingNetwork.x : this.getPointX(xPositionMultiplier);
+ const vnic = new VnicComponent(
+ vnicData,
+ new paper.Point(vnicPointX + vnicOffsetX, sharedPointY + vnicOffsetY));
+ this.addChild(vnic);
+ this._lowestVnicPointByNetworkName[vnicData.network_name] = this.localToGlobal(vnic.position);
+ if (!matchingNetwork) {
+ xPositionMultiplier++;
+ }
+ });
+
+ const vm = new VmComponent(
+ vmData,
+ new paper.Point(this.getPointX(xPositionMultiplier), sharedPointY),
+ true);
+ this.addChild(vm);
+ });
+ }
+
+ /**
+ * Gets array of VM data.
+ */
+ get data(): VmData[] {
+ return this._vms;
+ }
+
+ /**
+ * Gets the lowestVnicPointByNetworkName data.
+ */
+ get lowestVnicPointByNetworkName(): LowestVnicPointByNetworkName {
+ return this._lowestVnicPointByNetworkName;
+ }
+
+ /**
+ * Calculates x position for VMs and unattached VNICs.
+ * @param multiplier amount to stagger horizontal position by.
+ */
+ private getPointX(multiplier: number): number {
+ // multiply by the size of the vnic icon including margin and stroke
+ return multiplier * (CONNECTOR_SIZE + CONNECTOR_RIGHT_MARGIN + DEFAULT_STROKE_WIDTH * 2)
+ + this.lastNetworkPosition.x;
+ }
+}
diff --git a/src/components/vm.test.ts b/src/components/vm.test.ts
index 7e15ce2..da38dfc 100644
--- a/src/components/vm.test.ts
+++ b/src/components/vm.test.ts
@@ -1,7 +1,8 @@
import { VmComponent, VmData } from './vm';
import * as paper from 'paper';
import { LABEL_HORIZONTAL_PADDING, VM_ICON_SIZE } from '../constants/dimensions';
-import { FONT_SIZE, VERTICAL_PADDING_TOP } from './label';
+import { VERTICAL_PADDING_TOP } from './label';
+import { FONT_SIZE } from './label-text';
describe('vm component', () => {
@@ -12,26 +13,37 @@ describe('vm component', () => {
setRequestHeader: jest.fn()
});
(window as any).XMLHttpRequest = jest.fn().mockImplementation(xhrMockClass);
+ paper.settings.applyMatrix = false;
});
test('basic properties', () => {
const canvasEl = document.createElement('canvas');
paper.setup(canvasEl);
const vmData: VmData = {
- name: 'test',
uuid: 'uuid',
- operatingSystem: 'asianux3_64Guest'
+ name: 'sandbox.ts',
+ vapp_uuid: '',
+ operatingSystem: 'asianux3_64Guest',
+ vnics: [
+ {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: true
+ }
+ ]
};
const position = new paper.Point(60, 25);
const vm = new VmComponent(vmData, position);
- expect(vm.getVmData().name).toBe(vmData.name);
expect(vm.getVmData().uuid).toBe(vmData.uuid);
+ expect(vm.getVmData().name).toBe(vmData.name);
+ expect(vm.getVmData().vapp_uuid).toBe(vmData.vapp_uuid);
expect(vm.getVmData().operatingSystem).toBe(vmData.operatingSystem);
+ expect(vm.getVmData().vnics).toBe(vmData.vnics);
expect(vm.position.x).toBe(position.x);
expect(vm.position.y).toBe(position.y);
- expect(vm.getLabelComponent().getTextComponent().position.x).toBe(LABEL_HORIZONTAL_PADDING + VM_ICON_SIZE);
- expect(vm.getLabelComponent().getTextComponent().position.y).toBe(VERTICAL_PADDING_TOP + FONT_SIZE);
- expect(vm.getLabelComponent().getTextComponent().content).toBe(vmData.name);
+ expect(vm.getLabelComponent().text.position.x).toBe(LABEL_HORIZONTAL_PADDING + VM_ICON_SIZE);
+ expect(vm.getLabelComponent().text.position.y).toBe(VERTICAL_PADDING_TOP + FONT_SIZE);
+ expect(vm.getLabelComponent().text.content).toBe(vmData.name);
});
});
diff --git a/src/components/vm.ts b/src/components/vm.ts
index 0ef3e12..6ca357c 100644
--- a/src/components/vm.ts
+++ b/src/components/vm.ts
@@ -4,13 +4,16 @@ import { OperatingSystem } from 'iland-sdk';
import { IconLabelComponent } from './icon-label';
import { CanvasEventService } from '../services/canvas-event-service';
import { Subscription } from 'rxjs';
+import { VnicData } from './vnic';
const SIZE_DELTA_ON_HOVER = 2;
export interface VmData {
- operatingSystem: OperatingSystem;
- name: string;
uuid: string;
+ name: string;
+ vapp_uuid: string;
+ operatingSystem: OperatingSystem;
+ vnics: VnicData[];
}
/**
@@ -38,11 +41,11 @@ export class VmComponent extends paper.Group {
* @param visible whether the component is immediate visible (default is false because typically the component is
* rendered with a creation animation
*/
- constructor(private _vm: VmData, private _point: paper.Point = new paper.Point(0, 0),
+ constructor(private _vm: VmData,
+ private _point: paper.Point = new paper.Point(0, 0),
visible: boolean = false) {
super();
const self = this;
- this.applyMatrix = false;
this.position = _point;
this.pivot = new paper.Point(0, 0);
self._label = new IconLabelComponent(self._vm.name,
@@ -151,7 +154,7 @@ export class VmComponent extends paper.Group {
*/
private mouseLeave(event: paper.MouseEvent): void {
if (this.hovering) {
- const result = this.hitTest(event.point);
+ const result = this._label.hitTest(this.globalToLocal(event.point));
if (!result) {
this.hovering = false;
(this as any).tween({
@@ -181,7 +184,7 @@ export class VmComponent extends paper.Group {
}
/**
- * Handler for the containting canvas mouse down event.
+ * Handler for the containing canvas mouse down event.
* @param event {paper.MouseEvent}
*/
private canvasMouseDown(event: paper.MouseEvent): void {
diff --git a/src/components/vnic.test.ts b/src/components/vnic.test.ts
new file mode 100644
index 0000000..655bd7e
--- /dev/null
+++ b/src/components/vnic.test.ts
@@ -0,0 +1,44 @@
+import { VnicComponent, VnicData } from './vnic';
+import * as paper from 'paper';
+
+describe('vnic component', () => {
+
+ beforeAll(() => {
+ const canvasEl = document.createElement('canvas');
+ paper.setup(canvasEl);
+ paper.settings.applyMatrix = false;
+ });
+
+ test('basic properties', () => {
+ const vnicData: VnicData = {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: true
+ };
+ const position = new paper.Point(60, 25);
+ const vnic = new VnicComponent(vnicData, position);
+ expect(vnic.data.vnic_id).toBe(vnicData.vnic_id);
+ expect(vnic.data.network_name).toBe(vnicData.network_name);
+ expect(vnic.data.is_connected).toBe(vnicData.is_connected);
+ expect(vnic.position.x).toBe(position.x);
+ expect(vnic.position.y).toBe(position.y);
+ expect(vnic.children.length).toBe(1);
+ });
+
+ test('disconnected vnic', () => {
+ const vnicData: VnicData = {
+ vnic_id: 0,
+ network_name: 'A',
+ is_connected: false
+ };
+ const position = new paper.Point(-60, 25);
+ const vnic = new VnicComponent(vnicData, position);
+ expect(vnic.data.vnic_id).toBe(vnicData.vnic_id);
+ expect(vnic.data.network_name).toBe(vnicData.network_name);
+ expect(vnic.data.is_connected).toBe(vnicData.is_connected);
+ expect(vnic.position.x).toBe(position.x);
+ expect(vnic.position.y).toBe(position.y);
+ expect(vnic.children.length).toBe(3);
+ });
+
+});
diff --git a/src/components/vnic.ts b/src/components/vnic.ts
new file mode 100644
index 0000000..c31ccf9
--- /dev/null
+++ b/src/components/vnic.ts
@@ -0,0 +1,55 @@
+import * as paper from 'paper';
+import { ConnectionIconComponent } from './connection-icon';
+import { VAPP_BACKGROUND_COLOR } from '../constants/colors';
+import { DEFAULT_STROKE_STYLE } from '../constants/styles';
+
+/**
+ * Interface for vnic data.
+ */
+export interface VnicData {
+ vnic_id: number;
+ network_name: string;
+ is_connected: boolean;
+}
+
+/**
+ * Virtual Network Identifier Card Visual Component.
+ */
+export class VnicComponent extends paper.Group {
+
+ readonly icon: ConnectionIconComponent;
+
+ /**
+ * Creates a new VnicComponent instance.
+ * @param vnic The vnic data.
+ * @param _point The position where the vnic will be rendered at. Default is (0, 0).
+ */
+ constructor(private _vnic: VnicData,
+ private _point: paper.Point = new paper.Point(0, 0)) {
+ super();
+ this.position = _point;
+
+ this.icon = new ConnectionIconComponent();
+ this.addChild(this.icon);
+
+ // draw additional icon visual elements (slash and circle cut) if vnic is disconnected
+ if (!this._vnic.is_connected) {
+ let disconnectSlash = new paper.Path.Line(new paper.Point(-17 / 2, 0), new paper.Point(17 / 2, 0));
+ disconnectSlash.rotate(-45);
+ disconnectSlash.style = DEFAULT_STROKE_STYLE;
+ let disconnectCut = disconnectSlash.clone();
+ disconnectCut.style = {
+ strokeWidth: 6,
+ strokeColor: VAPP_BACKGROUND_COLOR
+ };
+ this.addChildren([disconnectCut, disconnectSlash]);
+ }
+ }
+
+ /**
+ * Gets the VnicData.
+ */
+ get data(): VnicData {
+ return this._vnic;
+ }
+}
diff --git a/src/constants/colors.ts b/src/constants/colors.ts
index 292a27e..1c23bcc 100644
--- a/src/constants/colors.ts
+++ b/src/constants/colors.ts
@@ -5,3 +5,4 @@
export const LIGHT_GREY = '#87A1B5';
export const VAPP_BACKGROUND_COLOR = '#343B4E';
export const CANVAS_BACKGROUND_COLOR = '#191C28';
+export const WHITE = '#FFFFFF';
diff --git a/src/constants/dimensions.ts b/src/constants/dimensions.ts
index 56355bd..9e0f925 100644
--- a/src/constants/dimensions.ts
+++ b/src/constants/dimensions.ts
@@ -2,6 +2,19 @@
* Dimensional constants that are shared among multiple components should be defined here.
*/
+export const DEFAULT_STROKE_WIDTH = 1;
+export const VIEW_BOTTOM_PADDING = 35;
+export const LABEL_HEIGHT = 30;
export const LABEL_HORIZONTAL_PADDING = 9;
+export const LABEL_BOTTOM_PADDING = 20;
+export const DEFAULT_MAX_LABEL_WIDTH = 200;
export const VM_ICON_SIZE = 30;
+export const VM_MARGIN_VERTICAL = 10;
+export const CONNECTOR_SIZE = 11;
+export const CONNECTOR_RADIUS = CONNECTOR_SIZE / 2;
+export const CONNECTOR_MARGIN = 10;
+export const CONNECTOR_RIGHT_MARGIN = 8;
+export const SMALL_CONNECTOR_SIZE = 7;
export const DEFAULT_SCROLLBAR_THICKNESS = 5;
+export const VAPP_PADDING = 20;
+export const VAPP_NETWORK_RIGHT_MARGIN = 20;
diff --git a/src/constants/styles.ts b/src/constants/styles.ts
new file mode 100644
index 0000000..e8ef6a2
--- /dev/null
+++ b/src/constants/styles.ts
@@ -0,0 +1,10 @@
+/**
+ * Style constants that are shared among multiple components should be defined here.
+ */
+import { DEFAULT_STROKE_WIDTH } from './dimensions';
+import { LIGHT_GREY } from './colors';
+
+export const DEFAULT_STROKE_STYLE = {
+ strokeWidth: DEFAULT_STROKE_WIDTH,
+ strokeColor: LIGHT_GREY
+};
diff --git a/src/gibraltar.ts b/src/gibraltar.ts
index 013cc82..3d279f6 100644
--- a/src/gibraltar.ts
+++ b/src/gibraltar.ts
@@ -19,6 +19,9 @@ export class Gibraltar {
this.canvas = el as HTMLCanvasElement;
}
paper.setup(this.canvas);
+ // apply this setting globally to applicable PaperJS types, so child components behave relatively to their parent
+ // component. disable on an individual basis by setting the item's applyMatrix property to `true`
+ paper.settings.applyMatrix = false;
}
}