From 4034463e765a5162f402f718da0cd415d52ae314 Mon Sep 17 00:00:00 2001 From: cho Date: Fri, 6 Sep 2019 10:44:51 -0500 Subject: [PATCH] Closes #22: add vapp static designs --- demo/src/app/app.component.html | 2 +- demo/src/app/app.component.ts | 2 +- .../components-page.component.html | 1 + .../components-page.component.less | 4 +- .../components/components-routing.module.ts | 4 + demo/src/app/components/components.module.ts | 4 + .../misc-page.component.ts | 4 +- ...isc-scrollbar-horizontal-demo.component.ts | 39 +- .../misc-scrollbar-vertical-demo.component.ts | 34 +- .../vapp-page.component.ts | 12 + .../vapp-static-demo.component.ts | 84 + .../vm-basic-demo.component.ts | 66 +- .../vm-create-demo.component.ts | 18 +- .../vm-delete-demo.component.ts | 18 +- .../constants/vapp-basic-placeholder-data.ts | 2064 +++++++++++++++++ demo/src/styles.less | 3 + src/.DS_Store | Bin 0 -> 8196 bytes src/components/connector.test.ts | 30 + src/components/connector.ts | 40 + src/components/entity-label.test.ts | 36 + src/components/entity-label.ts | 58 + src/components/isolated-network-label.test.ts | 29 + src/components/isolated-network-label.ts | 69 + src/components/label.ts | 30 +- src/components/margin.test.ts | 216 ++ src/components/margin.ts | 151 ++ src/components/scrollbar.test.ts | 2 +- src/components/scrollbar.ts | 73 +- src/components/small-connector.test.ts | 25 + src/components/small-connector.ts | 35 + src/components/vapp-edge-label.test.ts | 20 + src/components/vapp-edge-label.ts | 58 + src/components/vapp-network-list.test.ts | 61 + src/components/vapp-network-list.ts | 108 + src/components/vapp-network.test.ts | 110 + src/components/vapp-network.ts | 158 ++ src/components/vapp.test.ts | 81 + src/components/vapp.ts | 263 +++ src/components/vm-and-vnic-list.test.ts | 204 ++ src/components/vm-and-vnic-list.ts | 113 + src/components/vm.test.ts | 16 +- src/components/vm.ts | 14 +- src/components/vnic.test.ts | 43 + src/components/vnic.ts | 56 + src/constants/dimensions.ts | 13 + src/constants/styles.ts | 10 + 46 files changed, 4379 insertions(+), 102 deletions(-) create mode 100644 demo/src/app/components/vapp-page-component/vapp-page.component.ts create mode 100644 demo/src/app/components/vapp-static-demo-component/vapp-static-demo.component.ts create mode 100644 demo/src/app/constants/vapp-basic-placeholder-data.ts create mode 100644 src/.DS_Store create mode 100644 src/components/connector.test.ts create mode 100644 src/components/connector.ts create mode 100644 src/components/entity-label.test.ts create mode 100644 src/components/entity-label.ts create mode 100644 src/components/isolated-network-label.test.ts create mode 100644 src/components/isolated-network-label.ts create mode 100644 src/components/margin.test.ts create mode 100644 src/components/margin.ts create mode 100644 src/components/small-connector.test.ts create mode 100644 src/components/small-connector.ts create mode 100644 src/components/vapp-edge-label.test.ts create mode 100644 src/components/vapp-edge-label.ts create mode 100644 src/components/vapp-network-list.test.ts create mode 100644 src/components/vapp-network-list.ts create mode 100644 src/components/vapp-network.test.ts create mode 100644 src/components/vapp-network.ts create mode 100644 src/components/vapp.test.ts create mode 100644 src/components/vapp.ts create mode 100644 src/components/vm-and-vnic-list.test.ts create mode 100644 src/components/vm-and-vnic-list.ts create mode 100644 src/components/vnic.test.ts create mode 100644 src/components/vnic.ts create mode 100644 src/constants/styles.ts 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/app.component.ts b/demo/src/app/app.component.ts index d938432..2267106 100644 --- a/demo/src/app/app.component.ts +++ b/demo/src/app/app.component.ts @@ -3,7 +3,7 @@ import { Component } from '@angular/core'; @Component({ selector: 'app-root', template: require('./app.component.html'), - styles: [require('./app.component.less')] + styles: [require('../styles.less'), require('./app.component.less')] }) export class AppComponent { title = 'demo'; diff --git a/demo/src/app/components/components-page/components-page.component.html b/demo/src/app/components/components-page/components-page.component.html index a18e80c..715c169 100644 --- a/demo/src/app/components/components-page/components-page.component.html +++ b/demo/src/app/components/components-page/components-page.component.html @@ -3,6 +3,7 @@ diff --git a/demo/src/app/components/components-page/components-page.component.less b/demo/src/app/components/components-page/components-page.component.less index f71d695..3b04cdd 100644 --- a/demo/src/app/components/components-page/components-page.component.less +++ b/demo/src/app/components/components-page/components-page.component.less @@ -19,7 +19,7 @@ background-color: #E3E8E8 } .nav-link.active { - background-color: lightgray !important; - font-weight: 500 !important; + background-color: lightgray; + font-weight: 500; } } diff --git a/demo/src/app/components/components-routing.module.ts b/demo/src/app/components/components-routing.module.ts index 49e541a..7227dba 100644 --- a/demo/src/app/components/components-routing.module.ts +++ b/demo/src/app/components/components-routing.module.ts @@ -2,6 +2,7 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { ComponentsPageComponent } from './components-page/components-page.component'; import { VmPageComponent } from './vm-page-component/vm-page.component'; +import { VappPageComponent } from './vapp-page-component/vapp-page.component'; import { MiscPageComponent } from './misc-page-component/misc-page.component'; const routes = [ @@ -17,6 +18,9 @@ const routes = [ { path: 'vm', component: VmPageComponent }, + { + path: 'vapp', component: VappPageComponent + }, { path: 'misc', component: MiscPageComponent } diff --git a/demo/src/app/components/components.module.ts b/demo/src/app/components/components.module.ts index 1d79e24..a4421bf 100644 --- a/demo/src/app/components/components.module.ts +++ b/demo/src/app/components/components.module.ts @@ -9,6 +9,8 @@ import { DemoComponent } from './demo-component/demo.component'; import { VmPageComponent } from './vm-page-component/vm-page.component'; import { VmCreateDemoComponent } from './vm-create-demo-component/vm-create-demo.component'; import { VmDeleteDemoComponent } from './vm-delete-demo-component/vm-delete-demo.component'; +import { VappPageComponent } from './vapp-page-component/vapp-page.component'; +import { VappStaticDemoComponent } from './vapp-static-demo-component/vapp-static-demo.component'; import { MiscPageComponent } from './misc-page-component/misc-page.component'; import { MiscScrollbarHorizontalDemoComponent } from './misc-scrollbar-demo-component/misc-scrollbar-horizontal-demo.component'; @@ -23,6 +25,8 @@ import { MiscScrollbarVerticalDemoComponent } from VmBasicDemoComponent, ComponentsPageComponent, DemoComponent, + VappPageComponent, + VappStaticDemoComponent, MiscPageComponent, MiscScrollbarHorizontalDemoComponent, MiscScrollbarVerticalDemoComponent diff --git a/demo/src/app/components/misc-page-component/misc-page.component.ts b/demo/src/app/components/misc-page-component/misc-page.component.ts index 5a9481e..814efd4 100644 --- a/demo/src/app/components/misc-page-component/misc-page.component.ts +++ b/demo/src/app/components/misc-page-component/misc-page.component.ts @@ -1,9 +1,9 @@ import { Component } from '@angular/core'; @Component({ - selector: 'other-page', + selector: 'misc-page', template: ` -
+
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..ba41ad1 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 @@ -7,8 +7,8 @@ import { DEFAULT_SCROLLBAR_THICKNESS } from '../../../../../src/constants/dimens @Component({ selector: 'misc-scrollbar-horizontal-demo', - template: ` - ` }) @@ -26,7 +26,7 @@ export class MiscScrollbarHorizontalDemoComponent implements AfterViewInit { proj.activeLayer.applyMatrix = false; this.demo.backgroundColor = CANVAS_BACKGROUND_COLOR; const view = paper.view; - const canvas = this.demo.canvas.nativeElement; + const canvas = paper.view.element; const VIEW_PADDING = 30; // create content @@ -37,13 +37,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 +54,24 @@ 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, + containerBounds: view.bounds, + contentOffsetEnd: VIEW_PADDING + }, + new paper.Point(VIEW_PADDING, view.size.height - DEFAULT_SCROLLBAR_THICKNESS - 10), + view.bounds.width - VIEW_PADDING * 2 ); + if (scrollbar.isEnabled) { + canvas.onmouseenter = scrollbar.containerMouseEnter; + canvas.onmouseleave = scrollbar.containerMouseLeave; + + // add scroll listening. paper doesn't have a wheel event handler + canvas.onwheel = (event: WheelEvent) => { + scrollbar.onScroll(event); + }; + } - // 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 9e8f51c..3854b87 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,9 +1,8 @@ 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', @@ -26,7 +25,7 @@ export class MiscScrollbarVerticalDemoComponent implements AfterViewInit { proj.activeLayer.applyMatrix = false; this.demo.backgroundColor = CANVAS_BACKGROUND_COLOR; const view = paper.view; - const canvas = this.demo.canvas.nativeElement; + const canvas = paper.view.element; const VIEW_PADDING = 30; // create content @@ -54,20 +53,25 @@ 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, + containerBounds: view.bounds, + contentOffsetEnd: VIEW_PADDING + }, + new paper.Point(view.bounds.right - VIEW_PADDING, VIEW_PADDING), view.bounds.height - VIEW_PADDING * 2, - 'vertical'); + 'vertical' + ); + if (scrollbar.isEnabled) { + canvas.onmouseenter = scrollbar.containerMouseEnter; + canvas.onmouseleave = scrollbar.containerMouseLeave; + + // add scroll listening. paper doesn't have a wheel event handler + canvas.onwheel = (event: WheelEvent) => { + scrollbar.onScroll(event); + }; + } - // 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/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..633f312 --- /dev/null +++ b/demo/src/app/components/vapp-static-demo-component/vapp-static-demo.component.ts @@ -0,0 +1,84 @@ +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-basic-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 = paper.view; + const canvas = paper.view.element; + 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({ applyMatrix: false }); + // create origin paper Item for vapps to base position from + const origin = new paper.Path.Circle({ + position: new paper.Point(VIEW_PADDING, VERTICAL_POSITION), + radius: 0, + parent: content + }); + + // create vapps + vapps.forEach(vappData => { + const position = new paper.Point(content.lastChild.bounds.right, 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, + 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' + ); + if (horizontalScrollbar.isEnabled) { + canvas.onmouseenter = horizontalScrollbar.containerMouseEnter; + canvas.onmouseleave = horizontalScrollbar.containerMouseLeave; + } + + // add scroll listening. paper doesn't have a wheel event handler + canvas.onwheel = (event: WheelEvent) => { + // horizontal scrolling sent to horizontal scrollbar + if (Math.abs(event.deltaX) > Math.abs(event.deltaY)) { + horizontalScrollbar.onScroll(event); + } else { + // vertical scrolling sent to any scrollable vapp that's active/hovered + content.children.forEach(item => { + if (item instanceof VappComponent && item.isScrollable) { + item.setScrollListening(event); + } + }); + } + }; + + // TODO: keydown 'left' and 'right' should always go to horizontalScrollbar. keydown 'up' and 'down' to should go to + // any scrollable vapp that's active/hovered. can try handling with a paper tools service and/or tool stack + + // TODO: make sure 'Roboto' font loading finishes before canvas elements are rendered + } +} 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-basic-placeholder-data.ts b/demo/src/app/constants/vapp-basic-placeholder-data.ts new file mode 100644 index 0000000..0024156 --- /dev/null +++ b/demo/src/app/constants/vapp-basic-placeholder-data.ts @@ -0,0 +1,2064 @@ +import { VappData } from '../../../../src/components/vapp'; + +/** + * 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 +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: 'A', + is_connected: true + }, + { + vnic_id: 1, + network_name: '', + is_connected: false + }, + { + 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: [] + } +]; 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 0000000000000000000000000000000000000000..1ff6fd93f74e45982e7e599dd6bb762095ea5277 GIT binary patch literal 8196 zcmeHMTWl0n82-Pdz|0idQ{>Wixop}Xs6|>VpmK401FI!%XrV2|uCqI%j7(?i&TN;9 zjTK*vL43js>WdeQ!6y_GpNv5h?U5;1ln!4VEW#9kbp;QHh1;A5 zgfXc|rz1V3bQsEWs_X$#D54YtDxB>p>CQSG=_#dEI6;LIq9-HDP!K+y<5HnJA+2;Q zV+3LZu17%T?gr&Jb04GetMm74$sfCx3o@9>Y7%!Zr9$?;)zpdUU~Davu|JgBn%@&FBGa#{VVcQZq15} z>rZqjWD+mVhD@^i0i$Qg$>qI%=a`)l)jsdLo4b7ldxG~nn9IAxG0)G4nw;t8iY|lG z=DO0^9}4UvfovV{^M#-zbj%1*^zFSi`#BSKCwp_~5;;@vc;Mtcf4^PuSvRJe)#CMY zi1V=@qrcH~g>cy+qB#na>uWnD?lCxRjf~JR- z#x=?0h_rMRY?f9Oux@?Q81Q+o!i*f7HWngNmlabu#npxWwmad+&Wvh`K zoKRa94@QIAwj0TzTz+hfx8m2JDH=|r9lXyGQlQ5|_C(7j>f#bz*cj>elbtZFK&p z)ctXhtejTFHdd9HJd@%3 zn1A*WO_E3~RNkrj290?mG}T>OzfNOvi$vHpZm(xgQxo%)b)(UuFq? z@) { + + beforeAll(() => { + const canvasEl = document.createElement('canvas'); + paper.setup(canvasEl); + }); + + test('basic properties', () => { + const position = new paper.Point(0, 0); + const defaultColor = new paper.Color(VAPP_BACKGROUND_COLOR); + const connector = new ConnectorComponent(); + expect(connector.position.x).toBe(position.x); + expect(connector.position.y).toBe(position.y); + expect((connector.connector.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 ConnectorComponent(position, color); + expect(connector.position.x).toBe(position.x); + expect(connector.position.y).toBe(position.y); + expect((connector.connector.fillColor as paper.Color).equals(color)).toBe(true); + }); + +}); diff --git a/src/components/connector.ts b/src/components/connector.ts new file mode 100644 index 0000000..cd88c1b --- /dev/null +++ b/src/components/connector.ts @@ -0,0 +1,40 @@ +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'; + +/** + * Connector Visual Component. + */ +export class ConnectorComponent extends paper.Group { + + // the stroked circle item + readonly _connector: paper.Path.Circle; + + /** + * Creates a new ConnectorComponent instance. + * + * @param _point the location that the connector should be rendered at + * @param _fillColor the inner fill color that usually matches the background color of the element it is on top of + */ + constructor(private _point: paper.Point = new paper.Point(0, 0), + private _fillColor: paper.Color | string = VAPP_BACKGROUND_COLOR) { + super(); + this.applyMatrix = false; + this.position = _point; + this.pivot = new paper.Point(0, 0); + + this._connector = new paper.Path.Circle( + { + position: new paper.Point(0, 0), + radius: CONNECTOR_RADIUS, + style: DEFAULT_STROKE_STYLE, + fillColor: this._fillColor, + parent: this + }); + } + + get connector(): paper.Path.Circle { + return this._connector; + } +} diff --git a/src/components/entity-label.test.ts b/src/components/entity-label.test.ts new file mode 100644 index 0000000..e27a3d1 --- /dev/null +++ b/src/components/entity-label.test.ts @@ -0,0 +1,36 @@ +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); + }); + + 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('fontWeight', () => { + const text = 'foobar'; + const position = new paper.Point(10, 90); + const color = new paper.Color('blue'); + const fontWeight = 'bold'; + const label = new EntityLabelComponent(text, color, position, fontWeight); + expect(label.position.x).toBe(position.x); + expect(label.position.y).toBe(position.y); + expect(label.icon.fillColor).toBe(color); + expect(label.getTextComponent().fontWeight).toBe(fontWeight); + expect(label.bounds.right - LABEL_HORIZONTAL_PADDING) + .toBe(label.localToGlobal(label.getTextComponent().bounds.bottomRight).x); + }); + +}); diff --git a/src/components/entity-label.ts b/src/components/entity-label.ts new file mode 100644 index 0000000..6ad9d1c --- /dev/null +++ b/src/components/entity-label.ts @@ -0,0 +1,58 @@ +import * as paper from 'paper'; +import { LABEL_HORIZONTAL_PADDING } from '../constants/dimensions'; +import { LabelComponent } from './label'; + +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 fontWeight the font weight of the label. Defaults to 'normal' in parent + */ + constructor(protected _text: string, + private iconColor: paper.Color | string, + protected _point: paper.Point = new paper.Point(0, 0), + private _fontWeight: string | number = '') { + super(_text, _point); + this.applyMatrix = false; + 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 + }); + + // change in font weight will change the background width and possibly trigger clipping that will be + // handled by the parent + if (this._fontWeight) { + super.setFontWeight('bold'); + } + + // 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/isolated-network-label.test.ts b/src/components/isolated-network-label.test.ts new file mode 100644 index 0000000..4db5520 --- /dev/null +++ b/src/components/isolated-network-label.test.ts @@ -0,0 +1,29 @@ +import { IsolatedNetworkLabelComponent } from './isolated-network-label'; +import * as paper from 'paper'; + +describe('isolated network label component', () => { + + beforeAll(() => { + const canvasEl = document.createElement('canvas'); + paper.setup(canvasEl); + }); + + 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.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(200); + }); + +}); diff --git a/src/components/isolated-network-label.ts b/src/components/isolated-network-label.ts new file mode 100644 index 0000000..f3a3658 --- /dev/null +++ b/src/components/isolated-network-label.ts @@ -0,0 +1,69 @@ +import * as paper from 'paper'; +import { LIGHT_GREY } from '../constants/colors'; +import { SmallConnectorComponent } from './small-connector'; +import { DEFAULT_MAX_LABEL_WIDTH } from '../constants/dimensions'; + +const LABEL_PADDING_LEFT = 10; + +/** + * Isolated Network Label Visual Component. + */ +export class IsolatedNetworkLabelComponent extends paper.Group { + + readonly _label: paper.PointText; + // icon at the top of the isolated vApp network path + readonly icon: SmallConnectorComponent; + + /** + * Creates a new VappEdgeLabelComponent instance. + * + * @param network the network name + * @param _point the location that the vApp edge label should be rendered at + * @param maxWidth the maximum width of the label (text will be truncated with an ellipsis if the max width is + * exceeded) + */ + constructor(private network: string, + private _point: paper.Point = new paper.Point(0, 0), + private maxWidth: number = DEFAULT_MAX_LABEL_WIDTH) { + super(); + this.applyMatrix = false; + this.pivot = new paper.Point(0, 0); + this.position = _point; + + this.icon = new SmallConnectorComponent(); + this.addChild(this.icon); + + this._label = new paper.PointText({ + content: this.network.toUpperCase(), + fillColor: LIGHT_GREY, + fontWeight: 'bold', + fontSize: 10, + parent: this + }); + // 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.clip(); + } + + /** + * Gets the label. + */ + get label(): paper.PointText { + return this._label; + } + + // TODO: maxWidth and clip is reused from the generic label component. is there a better way to share these? + /** + * 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.ts b/src/components/label.ts index b42a3a5..bece167 100644 --- a/src/components/label.ts +++ b/src/components/label.ts @@ -1,7 +1,6 @@ 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'; -const HEIGHT = 30; const TEXT_COLOR = '#FFFFFF'; const TEXT_HOVER_COLOR = '#FFFFFF'; const ACTIVE_TEXT_COLOR = '#252A3A'; @@ -11,10 +10,10 @@ 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; +const FONT_FAMILY = 'Roboto'; /** - * LabelComponent Visual Component. + * Label Visual Component. */ export class LabelComponent extends paper.Group { @@ -31,10 +30,12 @@ export class LabelComponent extends paper.Group { * @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), - protected maxWidth = DEFAULT_MAX_LABEL_WIDTH) { + constructor(protected _text: string, + protected _point: paper.Point = new paper.Point(0, 0), + protected _maxWidth = DEFAULT_MAX_LABEL_WIDTH) { super(); this.applyMatrix = false; + 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)); @@ -44,10 +45,11 @@ export class LabelComponent extends paper.Group { this._label.fontSize = FONT_SIZE; this._label.leading = LINE_HEIGHT; this._label.pivot = new paper.Point(0, 0); + this._label.fontFamily = FONT_FAMILY; this.clip(); this._background = new paper.Path.Rectangle({ rectangle: new paper.Rectangle(0, 0, - this._label.bounds.width + (LABEL_HORIZONTAL_PADDING * 2), HEIGHT), + this._label.bounds.width + (LABEL_HORIZONTAL_PADDING * 2), LABEL_HEIGHT), radius: 3 }); this._background.fillColor = BACKGROUND_COLOR; @@ -70,6 +72,16 @@ export class LabelComponent extends paper.Group { return this._background; } + /** + * Sets the font weight for the label. Updates background width to fit font change and checks for clipping. + * @param weight String or number value for font weight. + */ + setFontWeight(weight: string | number) { + this._label.fontWeight = weight; + this.clip(); + this._background.bounds.width = this._label.bounds.width + (LABEL_HORIZONTAL_PADDING * 2); + } + /** * Sets the label to its visual hover state. */ @@ -87,7 +99,7 @@ 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; @@ -99,7 +111,7 @@ export class LabelComponent extends paper.Group { */ private clip() { let clipped = false; - while (this._label.bounds.width > this.maxWidth) { + while (this._label.bounds.width > this._maxWidth) { clipped = true; this._label.content = this._label.content.substring(0, this._label.content.length - 1); } diff --git a/src/components/margin.test.ts b/src/components/margin.test.ts new file mode 100644 index 0000000..33b17bc --- /dev/null +++ b/src/components/margin.test.ts @@ -0,0 +1,216 @@ +import { MarginComponent } from './margin'; +import * as paper from 'paper'; + +describe('margin component', () => { + + beforeAll(() => { + const canvasEl = document.createElement('canvas'); + paper.setup(canvasEl); + }); + + 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 one value', () => { + 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', () => { + 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', () => { + 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', () => { + 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', () => { + const content = createContent(); + const originalMarginValue = { + top: 10, + right: 20, + bottom: 15, + left: 30 + }; + const marginValue = { + top: 10, + right: 20, + bottom: 15, + left: 30 + }; + 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); + 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('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..b06e6bd --- /dev/null +++ b/src/components/margin.ts @@ -0,0 +1,151 @@ +import * as paper from 'paper'; + +const DEFAULT_MARGIN = 0; + +/** + * Interface for margin values. + */ +export interface MarginValues { + top: number; + right: number; + bottom: number; + left: number; +} + +/** + * Margin Component. + */ +export class MarginComponent extends paper.Group { + private marginValues: MarginValues; + private margin: paper.Path.Rectangle; + + /** + * Creates a new margin instance. + * + * @param content The content that the margin surrounds. + * @param marginValues Values for the margin sides. Can have a series of 0 to 4 that are comma separated in CSS + * shorthand order. Default is 0 for all margins. + */ + constructor(private content: paper.Item | paper.Group, ...marginValues: number[]) { + super(); + this.applyMatrix = false; + this.pivot = new paper.Point(0, 0); + + this.marginValues = this.assignMarginValues(marginValues); + this.createAndPositionMargin(); + } + + /** + * Sets new margin values. + * @param values Series of number values. Can have 0 to 4 that are comma separated in CSS shorthand order. Default + * is 0 for all margins. + */ + setValues(...values: number[]): void { + const top = this.marginValues.top; + const left = this.marginValues.left; + this.marginValues = this.assignMarginValues(values); + this.rebuildMargin(); + this.content.position.y += this.marginValues.top - top; + this.content.position.x += this.marginValues.left - left; + } + + /** + * 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 based on CSS shorthand style. + * @param values + */ + private assignMarginValues(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..1e4e311 100644 --- a/src/components/scrollbar.test.ts +++ b/src/components/scrollbar.test.ts @@ -3,7 +3,7 @@ import { ScrollbarComponent } from './scrollbar'; describe('scrollbar component', () => { - beforeEach(() => { + beforeAll(() => { const canvasEl = document.createElement('canvas'); paper.setup(canvasEl); }); diff --git a/src/components/scrollbar.ts b/src/components/scrollbar.ts index bbc3c0b..0a665c8 100644 --- a/src/components/scrollbar.ts +++ b/src/components/scrollbar.ts @@ -55,6 +55,8 @@ class CustomEffects { * Scrollbar Visual Component. */ export class ScrollbarComponent extends paper.Group { + + static anyIsDragging: boolean = false; protected scrollbar: paper.Path; protected track: paper.Path; protected scrollAmount: number; @@ -67,10 +69,14 @@ 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 isActivatable: boolean = false; + // scrollbar is enabled when content does not fit container + private _isEnabled: 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; @@ -151,13 +157,44 @@ 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; + } + + /** + * Gets isEnabled state. The scrollbar is enabled when content does not fit the container. + */ + get isEnabled(): boolean { + return this._isEnabled; + } + + /** + * Handler for container mouse enter event. + */ + containerMouseEnter = (): void => { + if (!ScrollbarComponent.anyIsDragging) { + this.isActivatable = true; + this.visible = true; + this.activateDefaultTool(); + } + } + + /** + * Handler for container mouse leave event. + */ + containerMouseLeave = (): void => { + if (!ScrollbarComponent.anyIsDragging) { + this.isActivatable = false; + this.visible = false; + } } /** * Activate the default tool. Used when the scrollable container is hovered or active. */ activateDefaultTool(): void { - if (this.isEnabled) { + if (this.isActivatable) { this.arrowKeyDown.activate(); } } @@ -176,7 +213,7 @@ export class ScrollbarComponent extends paper.Group { * }; */ onScroll(event: WheelEvent): void { - if (this.isEnabled) { + if (this.isActivatable) { const validScrollDirection = this.isHorizontal ? event.deltaX !== 0 : event.deltaY !== 0; if (!validScrollDirection) { return; @@ -290,15 +327,16 @@ export class ScrollbarComponent extends paper.Group { * Enable scrollbar visibility and interactivity. */ private enable(): void { - this.isEnabled = true; + this._isEnabled = true; this.addChildren([this.track, this.scrollbar, this.extendedTrackArea]); } /** - * Disable scrollbar visibility and interactivity. + * Disable scrollbar component elements and interactivity. */ private disable(): void { - this.isEnabled = false; + this.isActivatable = false; + this._isEnabled = false; this.removeChildren(); } @@ -385,7 +423,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 || 0) + - (this.scrollable.contentOffsetStart || 0); + return this.containerSize / fullSize * this.scrollTrackLength; } /** @@ -523,21 +563,32 @@ 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; + ScrollbarComponent.anyIsDragging = false; this.project.view.element.style.cursor = this.hovering ? 'pointer' : 'default'; + // set visibility based on if content is holding the current point + if (!this.scrollable.containerBounds.contains(event.point)) { + this.visible = false; + // TODO: less kludgey way of activating default tool. it's the last one created in the demo, but is a very + // temp fix. more tools can be added in the future, so this index won't always be correct + paper.tools[paper.tools.length - 1].activate(); + 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(); diff --git a/src/components/small-connector.test.ts b/src/components/small-connector.test.ts new file mode 100644 index 0000000..f183d24 --- /dev/null +++ b/src/components/small-connector.test.ts @@ -0,0 +1,25 @@ +import { SmallConnectorComponent } from './small-connector'; +import * as paper from 'paper'; + +describe('small connector component', () => { + + beforeAll(() => { + const canvasEl = document.createElement('canvas'); + paper.setup(canvasEl); + }); + + test('basic properties', () => { + const position = new paper.Point(0, 0); + const connector = new SmallConnectorComponent(); + 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 SmallConnectorComponent(position); + expect(connector.position.x).toBe(position.x); + expect(connector.position.y).toBe(position.y); + }); + +}); diff --git a/src/components/small-connector.ts b/src/components/small-connector.ts new file mode 100644 index 0000000..62168b0 --- /dev/null +++ b/src/components/small-connector.ts @@ -0,0 +1,35 @@ +import * as paper from 'paper'; +import { LIGHT_GREY } from '../constants/colors'; +import { SMALL_CONNECTOR_SIZE } from '../constants/dimensions'; + +/** + * Small Connector Visual Component. + */ +export class SmallConnectorComponent extends paper.Group { + + // the small filled circle + readonly _connector: paper.Path.Circle; + + /** + * Creates a new VappEdgeLabelComponent instance. + * + * @param _point the location that the vapp edge label should be rendered at + */ + constructor(private _point: paper.Point = new paper.Point(0, 0)) { + super(); + this.applyMatrix = false; + this.position = _point; + this.pivot = new paper.Point(0, 0); + + this._connector = new paper.Path.Circle({ + position: new paper.Point(0, 0), + radius: SMALL_CONNECTOR_SIZE / 2, + fillColor: LIGHT_GREY, + parent: this + }); + } + + get connector(): paper.Path.Circle { + return this._connector; + } +} diff --git a/src/components/vapp-edge-label.test.ts b/src/components/vapp-edge-label.test.ts new file mode 100644 index 0000000..c3dae2c --- /dev/null +++ b/src/components/vapp-edge-label.test.ts @@ -0,0 +1,20 @@ +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); + }); + + 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..0d3071f --- /dev/null +++ b/src/components/vapp-edge-label.ts @@ -0,0 +1,58 @@ +import * as paper from 'paper'; +import { EntityLabelComponent } from './entity-label'; +// import { VappNetworkData } from './vapp-network'; +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 vappEdge the vappEdge data + * @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.applyMatrix = false; + this.pivot = new paper.Point(0, 0); + this.position = _point; + + this._label = new EntityLabelComponent(this._text, ICON_COLOR); + this._label.getBackgroundComponent().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..74c342a --- /dev/null +++ b/src/components/vapp-network-list.test.ts @@ -0,0 +1,61 @@ +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); + }); + + 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..941bb03 --- /dev/null +++ b/src/components/vapp-network-list.ts @@ -0,0 +1,108 @@ +import * as paper from 'paper'; +import { VappNetworkData, VappNetworkComponent } from './vapp-network'; +import { LowestVnicPointByNetworkName } from './vm-and-vnic-list'; +import { DEFAULT_STROKE_STYLE } from '../constants/styles'; +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.applyMatrix = false; + 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 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) { + const clones = new paper.Group(); + clones.applyMatrix = false; + 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..abb3ee2 --- /dev/null +++ b/src/components/vapp-network.test.ts @@ -0,0 +1,110 @@ +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); + }); + + 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 small connector 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..f9b7193 --- /dev/null +++ b/src/components/vapp-network.ts @@ -0,0 +1,158 @@ +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 { ConnectorComponent } from './connector'; +import { SmallConnectorComponent } from './small-connector'; +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; + +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: ConnectorComponent | 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.applyMatrix = false; + 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 ConnectorComponent(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 SmallConnectorComponent(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..9c1f8af --- /dev/null +++ b/src/components/vapp.ts @@ -0,0 +1,263 @@ +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 = 10; +const EDGE_LABEL_BOTTOM_MARGIN = 5; + +/** + * 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 { + + readonly label: EntityLabelComponent; + readonly background: paper.Path.Rectangle; + readonly _margin: MarginComponent; + readonly vms: VmAndVnicListComponent; + readonly vappNetworks: VappNetworkListComponent; + // position for the division between any labels and vm/vnic list + readonly 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.applyMatrix = false; + 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), + '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, + 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(); + this.onMouseEnter = this.scrollMouseEnter; + this.onMouseLeave = this.scrollMouseLeave; + } + + // 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; + } + + /** + * Gets the isScrollable state when the vApp needs a scrollbar. + */ + get isScrollable(): boolean { + return this._isScrollable; + } + + /** + * Sets scroll listening if there's a scrollbar. + * @param event WheelEvent passed from the native HTML canvas. + */ + // TODO: add scroll listener with a better method. it's in parent demo component for now. could create a native canvas + // event observable service similar to the paper.event service? + setScrollListening(event: WheelEvent) { + if (this.scrollbar) { + this.scrollbar.onScroll(event); + } + } + + /** + * Handler for the mouse enter event when there is a scrollbar. + */ + private scrollMouseEnter = (): void => { + this.scrollbar.containerMouseEnter(); + } + + /** + * Handler for the mouse leave event when there is a scrollbar. + */ + private scrollMouseLeave = (): void => { + if (!ScrollbarComponent.anyIsDragging) { + this.scrollbar.containerMouseLeave(); + // TODO: less kludgey way of activating the global default paper tool. it's the last one created in the demo. more + // tools can be added or created in the future, so this index won't always be correct. can create a tool + // service and/or tool stack which creates and destroys paper.tools when items are in or out of the view + // activates the global default tool (view horizontal scrolling in the parent demo component) + paper.tools[paper.tools.length - 1].activate(); + } + } + + /** + * Clips and adds scrolling to the VmAndVnicList component when it's too large for the view. + */ + private clipAndScrollVmList() { + // 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.41), + shadowBlur: 10, + 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({ + applyMatrix: false, + children: [vappNetworkClone, this.vms] + }); + + // scrollbar set up + const scrollbarPadding = 5; + this.scrollbar = new ScrollbarComponent({ + content: scrollableContent, + 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' + ); + // drop shadow fades in or out onScroll + this.scrollbar.setCustomEffects({ + setActive: function() { + dropShadow.opacity = 1; + }, + setNormal: function() { + (dropShadow as any).tweenTo({ + opacity: 0 + }, 150); + } + }); + + // apply clip mask + // tslint:disable-next-line + new paper.Group({ + applyMatrix: false, + children: [vmListClipMask, scrollableContent, this.scrollbar, dropShadow], + clipped: true, + parent: this + }); + } +} 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..c4b248c --- /dev/null +++ b/src/components/vm-and-vnic-list.test.ts @@ -0,0 +1,204 @@ +import { VmAndVnicListComponent, LowestVnicPointByNetworkName } from './vm-and-vnic-list'; +import { VappNetworkPositionsByName } from './vapp-network-list'; +import { VmData } from './vm'; +import * as paper from 'paper'; +import { 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); + }); + + 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 + 4.5, // 4.5 VAPP_NETWORK_PADDING_RIGHT + networkCount * (LABEL_HEIGHT + VM_MARGIN_VERTICAL) + LABEL_HEIGHT / 2 + VM_MARGIN_VERTICAL + position.y); + networkCount++; + } + }); + }); + return expectedVnicPoints; + } + + test.only('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.y).toBe(position.y); + }); + + test.only('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, position); + expect(vms.lowestVnicPointByNetworkName).toEqual(getExpectedVnicPoints(vmsData, networkPositions, position)); + expect(vms.position.x).toBe(position.x); + expect(vms.position.y).toBe(position.y); + }); + + test.only('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, 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..eb1a334 --- /dev/null +++ b/src/components/vm-and-vnic-list.ts @@ -0,0 +1,113 @@ +import * as paper from 'paper'; +import { VmData, VmComponent } from './vm'; +import { VnicComponent } from './vnic'; +import { CONNECTOR_RADIUS, CONNECTOR_SIZE, CONNECTOR_RIGHT_PADDING, DEFAULT_STROKE_WIDTH, VAPP_NETWORK_RIGHT_MARGIN, + LABEL_HEIGHT, VM_MARGIN_VERTICAL } from '../constants/dimensions'; +import { VappNetworkPositionsByName } from './vapp-network-list'; + +const VAPP_NETWORK_PADDING_RIGHT = 4.5; + +/** + * 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 = {}; + // x position of the last (furthest right) network in vapp network list used for positioning vms and vnics + readonly lastNetworkPositionX: number = 0; + + /** + * Creates a new VmAndVnicListComponent instance. + * + * @param _vms the vms data + * @param vappNetworkPositionsByName the vapp network positions by name from the VappNetworkListComponent for + * positioning x value of matching vnics + * @param _point the location that the vm and vnic list should be rendered at + */ + constructor(private _vms: Array, + private vappNetworkPositionsByName: VappNetworkPositionsByName = {}, + private _point: paper.Point = new paper.Point(0, 0)) { + super(); + this.applyMatrix = false; + this.pivot = new paper.Point(0, 0); + this.position = _point; + + // reusable calculations + const vappNetworkCount = Object.keys(this.vappNetworkPositionsByName).length; + this.lastNetworkPositionX = vappNetworkCount && (vappNetworkCount - 1) * VAPP_NETWORK_RIGHT_MARGIN + + CONNECTOR_RADIUS - DEFAULT_STROKE_WIDTH; + const vnicOffsetX = vappNetworkCount + ? CONNECTOR_SIZE + CONNECTOR_RIGHT_PADDING + DEFAULT_STROKE_WIDTH * 2 + : VAPP_NETWORK_PADDING_RIGHT; + const vnicOffsetY = LABEL_HEIGHT / 2 + VM_MARGIN_VERTICAL; + const vmOffsetX = CONNECTOR_RADIUS + CONNECTOR_RIGHT_PADDING + DEFAULT_STROKE_WIDTH * 2; + + // draw vms and their vnics + this._vms.forEach((vmData, index) => { + const pointY = (LABEL_HEIGHT + VM_MARGIN_VERTICAL) * index; + vmData.vnics.forEach(vnicData => { + const vnic = new VnicComponent( + vnicData, + new paper.Point(this.getVnicPointX(vnicData.network_name, vnicOffsetX), pointY + vnicOffsetY)); + this.addChild(vnic); + this._lowestVnicPointByNetworkName[vnicData.network_name] = this.localToGlobal(vnic.position); + }); + const vm = new VmComponent( + vmData, + new paper.Point(this.getVmPointX(vmOffsetX),pointY + VM_MARGIN_VERTICAL), + 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 VNIC's x position based on if it's attached or unattached to a network and items drawn to the left + * of it. + * @param name the name of the vApp network that the VNIC is attached to + * @param offsetX the extra offset added to x position value + */ + private getVnicPointX(name: string, offsetX: number): number { + const pathPosition = this.vappNetworkPositionsByName && this.vappNetworkPositionsByName[name]; + if (pathPosition) { + // position is based on matching vapp network path + return pathPosition.x + VAPP_NETWORK_PADDING_RIGHT; + } else { + // position is based on the item (vnic or vapp network path) that is located furthest to right + const lastChildPositionX = this.lastChild && this.lastChild.position.x; + return Math.max(lastChildPositionX, this.lastNetworkPositionX) + offsetX; + } + } + + /** + * Calculate's VM's x position based on the item (vnic or vapp network path) drawn to the left of it. + * @param offsetX the extra offset added to x position value + */ + private getVmPointX(offsetX: number): number { + const lastVnicAndOffsetX = (this.lastChild instanceof VnicComponent) ? this.lastChild.position.x + offsetX : 0; + const lastNetworkAndOffsetX = this.lastNetworkPositionX && this.lastNetworkPositionX + offsetX; + // position is based on the item (vnic or vapp network path) that is located furthest to the right + return Math.max(lastVnicAndOffsetX, lastNetworkAndOffsetX); + } +} diff --git a/src/components/vm.test.ts b/src/components/vm.test.ts index 7e15ce2..e123847 100644 --- a/src/components/vm.test.ts +++ b/src/components/vm.test.ts @@ -18,15 +18,25 @@ describe('vm component', () => { 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); diff --git a/src/components/vm.ts b/src/components/vm.ts index 0ef3e12..917ebcc 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,7 +41,8 @@ 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; @@ -151,7 +155,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 +185,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..414ef45 --- /dev/null +++ b/src/components/vnic.test.ts @@ -0,0 +1,43 @@ +import { VnicComponent, VnicData } from './vnic'; +import * as paper from 'paper'; + +describe('vnic component', () => { + + beforeAll(() => { + const canvasEl = document.createElement('canvas'); + paper.setup(canvasEl); + }); + + 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..509aad5 --- /dev/null +++ b/src/components/vnic.ts @@ -0,0 +1,56 @@ +import * as paper from 'paper'; +import { ConnectorComponent } from './connector'; +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: ConnectorComponent; + + /** + * 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.applyMatrix = false; + this.position = _point; + + this.icon = new ConnectorComponent(); + 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/dimensions.ts b/src/constants/dimensions.ts index 56355bd..112f319 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_PADDING = 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 +};