Merge branch 'master' into custom-html-adbutler-ads

This commit is contained in:
Piotr Pekala 2019-08-02 00:49:03 -07:00
commit ebbeac6e0b
115 changed files with 4735 additions and 3326 deletions

View File

@ -14,7 +14,7 @@ init:
- git config --global core.autocrlf input
install:
- ps: Install-Product node 8 x64
- ps: Install-Product node 11 x64
- yarn install
build_script:

View File

@ -8,6 +8,11 @@ jobs:
steps:
- checkout
- run:
name: Install nodejs
command: |
brew upgrade node
- run:
name: Set timezone and check current datetime
command: |

View File

@ -1,7 +1,7 @@
language: node_js
node_js:
- '10' # use node for latest
- '11' # use node for latest
# Issue with Travis: https://github.com/travis-ci/travis-ci/issues/8836#issuecomment-356362524
sudo: required

View File

@ -93,24 +93,21 @@ If you would like to bump prepatch just type:
### Distribute release
We have got configured CircleCI, TravisCI and AppVeyor for distributing application for particular platform. In order to release you need to tag&push your changes.
We have got configured CircleCI, TravisCI and AppVeyor for distributing application for particular platform. In order to release you need to tag&push your changes from master.
Using `bump`:
First of all please remove `dev` from version in `package.json` (for instance `2019.2.0-alpha.4dev` to `2019.2.0-alpha.4`). Commit & push changes with message `Release 2019.2.0-alpha.4` . Next step is to tag repository and push to origin:
bump --patch --tag --push
Or manually:
git tag v0.0.1
git push origin v0.0.1
git tag v2019.2.0-alpha.4
git push origin v2019.2.0-alpha.4
When artifacts are made you can see draft release here: [gns3-web-ui releases](https://github.com/GNS3/gns3-web-ui/releases) which is waiting to be published.
After release please change current version in `package.json` to `X.X.X-beta.0`'. Otherwise artifacts will be overwritten during the next commit.
After release please change current version in `package.json` to `2019.2.0-alpha.5dev`'. Otherwise artifacts will be overwritten during the next commit. Don't forget to commit & push changes.
You may use `bump` to achieve that:
bump --prepatch
#### Updating gns3server
Checkout the latest master of `gns3server`. Run command `./scripts/update-bundled-web-ui.sh --tag=v2019.2.0-alpha.5`. Commit & push changes with message `Release 2019.2.0-alpha.4`.
### Staging release

View File

@ -18,7 +18,8 @@
"polyfills": "src/polyfills.ts",
"assets": [
"src/assets",
"src/favicon.ico"
"src/favicon.ico",
"src/ReleaseNotes.txt"
],
"styles": [
"node_modules/bootstrap/dist/css/bootstrap.min.css",

View File

@ -1,6 +1,6 @@
{
"name": "gns3-web-ui",
"version": "2019.2.0-alpha.3dev",
"version": "2019.2.0-alpha.6dev",
"author": {
"name": "GNS3 Technology Inc.",
"email": "developers@gns3.com"
@ -38,73 +38,73 @@
},
"private": true,
"dependencies": {
"@angular/animations": "^7.2.14",
"@angular/cdk": "^7.3.7",
"@angular/common": "^7.2.14",
"@angular/compiler": "^7.2.14",
"@angular/core": "^7.2.14",
"@angular/forms": "^7.2.14",
"@angular/http": "^7.2.14",
"@angular/material": "^7.3.7",
"@angular/platform-browser": "^7.2.14",
"@angular/platform-browser-dynamic": "^7.2.14",
"@angular/router": "^7.2.14",
"@angular/animations": "^8.1.2",
"@angular/cdk": "^8.1.1",
"@angular/common": "^8.1.2",
"@angular/compiler": "^8.1.2",
"@angular/core": "^8.1.2",
"@angular/forms": "^8.1.2",
"@angular/http": "^7.2.15",
"@angular/material": "^8.1.1",
"@angular/platform-browser": "^8.1.2",
"@angular/platform-browser-dynamic": "^8.1.2",
"@angular/router": "^8.1.2",
"angular-persistence": "^1.0.1",
"angular2-hotkeys": "^2.1.4",
"angular2-indexeddb": "^1.2.3",
"bootstrap": "4.3.1",
"command-exists": "^1.2.8",
"core-js": "^3.0.1",
"css-tree": "^1.0.0-alpha.29",
"core-js": "^3.1.4",
"css-tree": "^1.0.0-alpha.33",
"d3-ng2-service": "^2.1.0",
"hammerjs": "^2.0.8",
"ini": "^1.3.5",
"material-design-icons": "^3.0.1",
"ng2-file-upload": "^1.3.0",
"ngx-electron": "^2.1.1",
"node-fetch": "^2.4.1",
"node-fetch": "^2.6.0",
"notosans-fontface": "^1.1.0",
"raven-js": "^3.27.0",
"rxjs": "^6.5.1",
"rxjs-compat": "^6.5.1",
"raven-js": "^3.27.2",
"rxjs": "^6.5.2",
"rxjs-compat": "^6.5.2",
"tree-kill": "^1.2.1",
"typeface-roboto": "^0.0.54",
"yargs": "^13.2.2",
"zone.js": "^0.9.0"
"typeface-roboto": "^0.0.75",
"yargs": "^13.3.0",
"zone.js": "^0.9.1"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.13.8",
"@angular/cli": "^7.3.8",
"@angular/compiler-cli": "^7.2.14",
"@angular/language-service": "^7.2.14",
"@sentry/cli": "^1.41.2",
"@sentry/electron": "^0.17.1",
"@types/jasmine": "~3.3.12",
"@angular-devkit/build-angular": "~0.801.2",
"@angular/cli": "^8.1.2",
"@angular/compiler-cli": "^8.1.2",
"@angular/language-service": "^8.1.2",
"@sentry/cli": "^1.47.0",
"@sentry/electron": "^0.17.3",
"@types/jasmine": "~3.3.15",
"@types/jasminewd2": "~2.0.6",
"@types/node": "~12.0.0",
"codelyzer": "~5.0.1",
"electron": "5.0.1",
"electron-builder": "20.39.0",
"@types/node": "~12.6.8",
"codelyzer": "~5.1.0",
"electron": "5.0.8",
"electron-builder": "21.1.5",
"jasmine-core": "~3.4.0",
"jasmine-spec-reporter": "~4.2.1",
"jquery": "^3.4.0",
"karma": "~4.1.0",
"karma-chrome-launcher": "~2.2.0",
"jquery": "^3.4.1",
"karma": "~4.2.0",
"karma-chrome-launcher": "~3.0.0",
"karma-cli": "~2.0.0",
"karma-coverage-istanbul-reporter": "^2.0.5",
"karma-coverage-istanbul-reporter": "^2.1.0",
"karma-jasmine": "~2.0.1",
"karma-jasmine-html-reporter": "^1.4.2",
"license-checker": "^25.0.1",
"node-sass": "^4.12.0",
"popper.js": "^1.15.0",
"prettier": "^1.17.0",
"prettier": "^1.18.2",
"protractor": "~5.4.2",
"replace": "^1.1.0",
"ts-mockito": "^2.3.1",
"ts-node": "~8.1.0",
"tslint": "~5.16.0",
"ts-mockito": "^2.4.2",
"ts-node": "~8.3.0",
"tslint": "~5.18.0",
"tslint-config-prettier": "^1.18.0",
"typescript": "<3.3.0"
"typescript": ">=3.4.0 <3.5.0"
},
"greenkeeper": {
"ignore": [

View File

@ -44,7 +44,7 @@ DEPENDENCIES = {
'ubridge': {
'type': 'github',
'releases': 'https://api.github.com/repos/GNS3/ubridge/releases',
'version': '0.9.15', # possible to use LATEST as value
'version': '0.9.16', # possible to use LATEST as value
'files': {
'windows': [
'cygwin1.dll',

16
src/ReleaseNotes.txt Normal file
View File

@ -0,0 +1,16 @@
GNS3 WebUI is web implementation of user interface for GNS3 software.
Current version: 2019.2.0
What's New
- Help section added with information about third party components
- Showing progress when server starting
- Possibility to edit interface & node labels by using context menu
- Enhancements in moving elements on map
- Context menu extended with option to duplicate
- Main menu extended with option to lock all items on map
Bug Fixes
- Removing issues with positioning interface labels while adding link between nodes on map
- Context menu now is correctly placed
- Entered text in text & style editor is now validated

View File

@ -52,6 +52,7 @@ import { CopyDockerTemplateComponent } from './components/preferences/docker/cop
import { CopyIouTemplateComponent } from './components/preferences/ios-on-unix/copy-iou-template/copy-iou-template.component';
import { ListOfSnapshotsComponent } from './components/snapshots/list-of-snapshots/list-of-snapshots.component';
import { ConsoleComponent } from './components/settings/console/console.component';
import { HelpComponent } from './components/help/help.component';
const routes: Routes = [
{
@ -62,6 +63,7 @@ const routes: Routes = [
{ path: 'servers', component: ServersComponent },
{ path: 'bundled', component: BundledServerFinderComponent },
{ path: 'server/:server_id/projects', component: ProjectsComponent },
{ path: 'help', component: HelpComponent },
{ path: 'settings', component: SettingsComponent },
{ path: 'settings/console', component: ConsoleComponent },
{ path: 'installed-software', component: InstalledSoftwareComponent },

View File

@ -180,11 +180,18 @@ import { StartCaptureDialogComponent } from './components/project-map/packet-cap
import { SuspendLinkActionComponent } from './components/project-map/context-menu/actions/suspend-link/suspend-link-action.component';
import { ResumeLinkActionComponent } from './components/project-map/context-menu/actions/resume-link-action/resume-link-action.component';
import { StopCaptureActionComponent } from './components/project-map/context-menu/actions/stop-capture/stop-capture-action.component';
import { MapScaleService } from './services/mapScale.service';
import { AdbutlerComponent } from './components/adbutler/adbutler.component';
import { ConsoleService } from './services/settings/console.service';
import { DefaultConsoleService } from './services/settings/default-console.service';
import { NodeCreatedLabelStylesFixer } from './components/project-map/helpers/node-created-label-styles-fixer';
import { NotificationBoxComponent } from './components/notification-box/notification-box.component';
import { NonNegativeValidator } from './validators/non-negative-validator';
import { RotationValidator } from './validators/rotation-validator';
import { DuplicateActionComponent } from './components/project-map/context-menu/actions/duplicate-action/duplicate-action.component';
import { MapSettingService } from './services/mapsettings.service';
import { ProjectMapMenuComponent } from './components/project-map/project-map-menu/project-map-menu.component';
import { HelpComponent } from './components/help/help.component';
if (environment.production) {
Raven.config('https://b2b1cfd9b043491eb6b566fd8acee358@sentry.io/842726', {
@ -219,6 +226,7 @@ if (environment.production) {
EditStyleActionComponent,
EditTextActionComponent,
DeleteActionComponent,
DuplicateActionComponent,
PacketFiltersActionComponent,
StartCaptureActionComponent,
StopCaptureActionComponent,
@ -304,7 +312,9 @@ if (environment.production) {
ConsoleDeviceActionComponent,
ConsoleComponent,
NodesMenuComponent,
NotificationBoxComponent
NotificationBoxComponent,
ProjectMapMenuComponent,
HelpComponent
],
imports: [
BrowserModule,
@ -374,9 +384,13 @@ if (environment.production) {
IouConfigurationService,
RecentlyOpenedProjectService,
ServerManagementService,
MapScaleService,
ConsoleService,
DefaultConsoleService,
NodeCreatedLabelStylesFixer
NodeCreatedLabelStylesFixer,
NonNegativeValidator,
RotationValidator,
MapSettingService
],
entryComponents: [
AddServerDialogComponent,

View File

@ -55,6 +55,9 @@ import { RectangleElementFactory } from './helpers/drawings-factory/rectangle-el
import { LineElementFactory } from './helpers/drawings-factory/line-element-factory';
import { TextEditorComponent } from './components/text-editor/text-editor.component';
import { DrawingAddingComponent } from './components/drawing-adding/drawing-adding.component';
import { MovingEventSource } from './events/moving-event-source';
import { MovingCanvasDirective } from './directives/moving-canvas.directive';
import { ZoomingCanvasDirective } from './directives/zooming-canvas.directive';
@NgModule({
imports: [CommonModule, MatMenuModule, MatIconModule],
@ -67,7 +70,9 @@ import { DrawingAddingComponent } from './components/drawing-adding/drawing-addi
...ANGULAR_MAP_DECLARATIONS,
SelectionControlComponent,
SelectionSelectComponent,
DraggableSelectionComponent
DraggableSelectionComponent,
MovingCanvasDirective,
ZoomingCanvasDirective
],
providers: [
CssFixer,
@ -87,6 +92,7 @@ import { DrawingAddingComponent } from './components/drawing-adding/drawing-addi
DrawingsEventSource,
NodesEventSource,
LinksEventSource,
MovingEventSource,
MapDrawingToSvgConverter,
DrawingToMapDrawingConverter,
LabelToMapLabelConverter,

View File

@ -1,4 +1,4 @@
<svg #svg class="map" preserveAspectRatio="none">
<svg #svg class="map" preserveAspectRatio="none" movingCanvas zoomingCanvas>
<filter id="grayscale"><feColorMatrix id="feGrayscale" type="saturate" values="0" /></filter>
</svg>
@ -6,5 +6,5 @@
<app-drawing-resizing></app-drawing-resizing>
<app-selection-control></app-selection-control>
<app-selection-select></app-selection-select>
<app-text-editor #textEditor [svg]="svg"></app-text-editor>
<app-text-editor #textEditor [server]="server" [svg]="svg"></app-text-editor>
<app-draggable-selection [svg]="svg"></app-draggable-selection>

Before

Width:  |  Height:  |  Size: 472 B

After

Width:  |  Height:  |  Size: 517 B

View File

@ -31,6 +31,7 @@ import { MapSettingsManager } from '../../managers/map-settings-manager';
import { Server } from '../../../models/server';
import { ToolsService } from '../../../services/tools.service';
import { TextEditorComponent } from '../text-editor/text-editor.component';
import { MapScaleService } from '../../../services/mapScale.service';
@Component({
selector: 'app-d3-map',
@ -47,8 +48,8 @@ export class D3MapComponent implements OnInit, OnChanges, OnDestroy {
@Input() width = 1500;
@Input() height = 600;
@ViewChild('svg') svgRef: ElementRef;
@ViewChild('textEditor') textEditor: TextEditorComponent;
@ViewChild('svg', {static: false}) svgRef: ElementRef;
@ViewChild('textEditor', {static: false}) textEditor: TextEditorComponent;
private parentNativeElement: any;
private svg: Selection<SVGSVGElement, any, null, undefined>;
@ -73,7 +74,8 @@ export class D3MapComponent implements OnInit, OnChanges, OnDestroy {
protected selectionToolWidget: SelectionTool,
protected movingToolWidget: MovingTool,
public graphLayout: GraphLayout,
private toolsService: ToolsService
private toolsService: ToolsService,
private mapScaleService: MapScaleService
) {
this.parentNativeElement = element.nativeElement;
}
@ -119,9 +121,12 @@ export class D3MapComponent implements OnInit, OnChanges, OnDestroy {
}
});
this.subscriptions.push(
this.mapScaleService.scaleChangeEmitter.subscribe((value: number) => this.redraw())
);
this.subscriptions.push(
this.toolsService.isMovingToolActivated.subscribe((value: boolean) => {
this.movingToolWidget.setEnabled(value);
this.mapChangeDetectorRef.detectChanges();
})
);
@ -180,7 +185,8 @@ export class D3MapComponent implements OnInit, OnChanges, OnDestroy {
this.graphDataManager.setLinks(this.links);
this.graphDataManager.setDrawings(this.drawings);
this.graphLayout.draw(this.svg, this.context);
this.textEditor.activateTextEditing();
this.textEditor.activateTextEditingForDrawings();
this.textEditor.activateTextEditingForNodeLabels();
}
@HostListener('window:resize', ['$event'])

View File

@ -21,6 +21,7 @@ import { MapLabel } from '../../models/map/map-label';
import { MapLinkNode } from '../../models/map/map-link-node';
import { select } from 'd3-selection';
import { MapLink } from '../../models/map/map-link';
import { MapSettingService } from '../../../services/mapsettings.service';
describe('DraggableSelectionComponent', () => {
let component: DraggableSelectionComponent;
@ -121,7 +122,8 @@ describe('DraggableSelectionComponent', () => {
{ provide: NodesEventSource, useValue: nodesEventSourceStub },
{ provide: DrawingsEventSource, useValue: drawingsEventSourceStub },
{ provide: GraphDataManager, useValue: mockedGraphDataManager },
{ provide: LinksEventSource, useValue: linksEventSourceStub }
{ provide: LinksEventSource, useValue: linksEventSourceStub },
{ provide: MapSettingService, useClass: MapSettingService }
],
declarations: [DraggableSelectionComponent]
}).compileComponents();

View File

@ -17,6 +17,7 @@ import { LabelWidget } from '../../widgets/label';
import { InterfaceLabelWidget } from '../../widgets/interface-label';
import { MapLinkNode } from '../../models/map/map-link-node';
import { LinksEventSource } from '../../events/links-event-source';
import { MapSettingService } from '../../../services/mapsettings.service';
@Component({
selector: 'app-draggable-selection',
@ -27,6 +28,8 @@ export class DraggableSelectionComponent implements OnInit, OnDestroy {
private start: Subscription;
private drag: Subscription;
private end: Subscription;
private mapSettingsSubscription: Subscription;
private isMapLocked: boolean = false;
@Input('svg') svg: SVGSVGElement;
@ -40,12 +43,17 @@ export class DraggableSelectionComponent implements OnInit, OnDestroy {
private nodesEventSource: NodesEventSource,
private drawingsEventSource: DrawingsEventSource,
private graphDataManager: GraphDataManager,
private linksEventSource: LinksEventSource
private linksEventSource: LinksEventSource,
private mapSettingsService: MapSettingService
) {}
ngOnInit() {
const svg = select(this.svg);
this.mapSettingsSubscription = this.mapSettingsService.isMapLocked.subscribe((value) => {
this.isMapLocked = value;
});
this.start = merge(
this.nodesWidget.draggable.start,
this.drawingsWidget.draggable.start,
@ -84,75 +92,77 @@ export class DraggableSelectionComponent implements OnInit, OnDestroy {
this.labelWidget.draggable.drag,
this.interfaceWidget.draggable.drag
).subscribe((evt: DraggableDrag<any>) => {
const selected = this.selectionManager.getSelected();
const selectedNodes = selected.filter(item => item instanceof MapNode);
// update nodes
selectedNodes.forEach((node: MapNode) => {
node.x += evt.dx;
node.y += evt.dy;
if (!this.isMapLocked) {
const selected = this.selectionManager.getSelected();
const selectedNodes = selected.filter(item => item instanceof MapNode);
// update nodes
selectedNodes.forEach((node: MapNode) => {
node.x += evt.dx;
node.y += evt.dy;
this.nodesWidget.redrawNode(svg, node);
this.nodesWidget.redrawNode(svg, node);
const links = this.graphDataManager
.getLinks()
.filter(
link =>
(link.target !== undefined && link.target.id === node.id) ||
(link.source !== undefined && link.source.id === node.id)
);
links.forEach(link => {
this.linksWidget.redrawLink(svg, link);
});
});
// update drawings
selected
.filter(item => item instanceof MapDrawing)
.forEach((drawing: MapDrawing) => {
drawing.x += evt.dx;
drawing.y += evt.dy;
this.drawingsWidget.redrawDrawing(svg, drawing);
});
// update labels
selected
.filter(item => item instanceof MapLabel)
.forEach((label: MapLabel) => {
const isParentNodeSelected = selectedNodes.filter(node => node.id === label.nodeId).length > 0;
if (isParentNodeSelected) {
return;
}
const node = this.graphDataManager.getNodes().filter(node => node.id === label.nodeId)[0];
node.label.x += evt.dx;
node.label.y += evt.dy;
this.labelWidget.redrawLabel(svg, label);
});
// update interface labels
selected
.filter(item => item instanceof MapLinkNode)
.forEach((interfaceLabel: MapLinkNode) => {
const isParentNodeSelected = selectedNodes.filter(node => node.id === interfaceLabel.nodeId).length > 0;
if (isParentNodeSelected) {
return;
}
const link = this.graphDataManager
const links = this.graphDataManager
.getLinks()
.filter(link => link.nodes[0].id === interfaceLabel.id || link.nodes[1].id === interfaceLabel.id)[0];
if (link.nodes[0].id === interfaceLabel.id) {
link.nodes[0].label.x += evt.dx;
link.nodes[0].label.y += evt.dy;
}
if (link.nodes[1].id === interfaceLabel.id) {
link.nodes[1].label.x += evt.dx;
link.nodes[1].label.y += evt.dy;
}
.filter(
link =>
(link.target !== undefined && link.target.id === node.id) ||
(link.source !== undefined && link.source.id === node.id)
);
this.linksWidget.redrawLink(svg, link);
links.forEach(link => {
this.linksWidget.redrawLink(svg, link);
});
});
// update drawings
selected
.filter(item => item instanceof MapDrawing)
.forEach((drawing: MapDrawing) => {
drawing.x += evt.dx;
drawing.y += evt.dy;
this.drawingsWidget.redrawDrawing(svg, drawing);
});
// update labels
selected
.filter(item => item instanceof MapLabel)
.forEach((label: MapLabel) => {
const isParentNodeSelected = selectedNodes.filter(node => node.id === label.nodeId).length > 0;
if (isParentNodeSelected) {
return;
}
const node = this.graphDataManager.getNodes().filter(node => node.id === label.nodeId)[0];
node.label.x += evt.dx;
node.label.y += evt.dy;
this.labelWidget.redrawLabel(svg, label);
});
// update interface labels
selected
.filter(item => item instanceof MapLinkNode)
.forEach((interfaceLabel: MapLinkNode) => {
const isParentNodeSelected = selectedNodes.filter(node => node.id === interfaceLabel.nodeId).length > 0;
if (isParentNodeSelected) {
return;
}
const link = this.graphDataManager
.getLinks()
.filter(link => link.nodes[0].id === interfaceLabel.id || link.nodes[1].id === interfaceLabel.id)[0];
if (link.nodes[0].id === interfaceLabel.id) {
link.nodes[0].label.x += evt.dx;
link.nodes[0].label.y += evt.dy;
}
if (link.nodes[1].id === interfaceLabel.id) {
link.nodes[1].label.x += evt.dx;
link.nodes[1].label.y += evt.dy;
}
this.linksWidget.redrawLink(svg, link);
});
}
});
this.end = merge(
@ -161,39 +171,41 @@ export class DraggableSelectionComponent implements OnInit, OnDestroy {
this.labelWidget.draggable.end,
this.interfaceWidget.draggable.end
).subscribe((evt: DraggableEnd<any>) => {
const selected = this.selectionManager.getSelected();
const selectedNodes = selected.filter(item => item instanceof MapNode);
if (!this.isMapLocked) {
const selected = this.selectionManager.getSelected();
const selectedNodes = selected.filter(item => item instanceof MapNode);
selectedNodes.forEach((item: MapNode) => {
this.nodesEventSource.dragged.emit(new DraggedDataEvent<MapNode>(item, evt.dx, evt.dy));
});
selected
.filter(item => item instanceof MapDrawing)
.forEach((item: MapDrawing) => {
this.drawingsEventSource.dragged.emit(new DraggedDataEvent<MapDrawing>(item, evt.dx, evt.dy));
selectedNodes.forEach((item: MapNode) => {
this.nodesEventSource.dragged.emit(new DraggedDataEvent<MapNode>(item, evt.dx, evt.dy));
});
selected
.filter(item => item instanceof MapLabel)
.forEach((label: MapLabel) => {
const isParentNodeSelected = selectedNodes.filter(node => node.id === label.nodeId).length > 0;
if (isParentNodeSelected) {
return;
}
selected
.filter(item => item instanceof MapDrawing)
.forEach((item: MapDrawing) => {
this.drawingsEventSource.dragged.emit(new DraggedDataEvent<MapDrawing>(item, evt.dx, evt.dy));
});
this.nodesEventSource.labelDragged.emit(new DraggedDataEvent<MapLabel>(label, evt.dx, evt.dy));
});
selected
.filter(item => item instanceof MapLabel)
.forEach((label: MapLabel) => {
const isParentNodeSelected = selectedNodes.filter(node => node.id === label.nodeId).length > 0;
if (isParentNodeSelected) {
return;
}
selected
.filter(item => item instanceof MapLinkNode)
.forEach((label: MapLinkNode) => {
const isParentNodeSelected = selectedNodes.filter(node => node.id === label.nodeId).length > 0;
if (isParentNodeSelected) {
return;
}
this.linksEventSource.interfaceDragged.emit(new DraggedDataEvent<MapLinkNode>(label, evt.dx, evt.dy));
});
this.nodesEventSource.labelDragged.emit(new DraggedDataEvent<MapLabel>(label, evt.dx, evt.dy));
});
selected
.filter(item => item instanceof MapLinkNode)
.forEach((label: MapLinkNode) => {
const isParentNodeSelected = selectedNodes.filter(node => node.id === label.nodeId).length > 0;
if (isParentNodeSelected) {
return;
}
this.linksEventSource.interfaceDragged.emit(new DraggedDataEvent<MapLinkNode>(label, evt.dx, evt.dy));
});
}
});
}
@ -201,5 +213,6 @@ export class DraggableSelectionComponent implements OnInit, OnDestroy {
this.start.unsubscribe();
this.drag.unsubscribe();
this.end.unsubscribe();
this.mapSettingsSubscription.unsubscribe();
}
}

View File

@ -25,8 +25,8 @@ export class DrawingAddingComponent implements OnInit, OnDestroy {
activate() {
let listener = (event: MouseEvent) => {
let x = event.pageX - (this.context.getZeroZeroTransformationPoint().x + this.context.transformation.x);
let y = event.pageY - (this.context.getZeroZeroTransformationPoint().y + this.context.transformation.y);
let x = (event.pageX - (this.context.getZeroZeroTransformationPoint().x + this.context.transformation.x))/this.context.transformation.k;
let y = (event.pageY - (this.context.getZeroZeroTransformationPoint().y + this.context.transformation.y))/this.context.transformation.k;
this.drawingsEventSource.pointToAddSelected.emit(new AddedDataEvent(x, y));
this.deactivate();

View File

@ -13,7 +13,7 @@ export class TextComponent implements OnInit, DoCheck {
@Input('app-text') text: TextElement;
@ViewChild('text') textRef: ElementRef;
@ViewChild('text', {static: false}) textRef: ElementRef;
lines: string[] = [];

View File

@ -43,7 +43,7 @@ export class ExperimentalMapComponent implements OnInit, OnChanges, OnDestroy {
@Input() width = 1500;
@Input() height = 600;
@ViewChild('svg') svg: ElementRef;
@ViewChild('svg', {static: false}) svg: ElementRef;
private changesDetected: Subscription;

View File

@ -10,7 +10,7 @@ import { CssFixer } from '../../../helpers/css-fixer';
export class InterfaceLabelComponent implements OnInit {
@Input('app-interface-label') ignore: any;
@ViewChild('textSvg') textRef: ElementRef;
@ViewChild('textSvg', {static: false}) textRef: ElementRef;
private label = {
x: 0,

View File

@ -26,7 +26,7 @@ export class LinkComponent implements OnInit, OnDestroy {
@Input('node-changed') nodeChanged: EventEmitter<Node>;
@Input('show-interface-labels') showInterfaceLabels: boolean;
@ViewChild('path') path: ElementRef;
@ViewChild('path', {static: false}) path: ElementRef;
private ethernetLinkStrategy = new EthernetLinkStrategy();
private serialLinkStrategy = new SerialLinkStrategy();

View File

@ -30,8 +30,8 @@ import { DraggedDataEvent } from '../../../events/event-source';
export class NodeComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit {
static NODE_LABEL_MARGIN = 3;
@ViewChild('label') label: ElementRef;
@ViewChild('image') imageRef: ElementRef;
@ViewChild('label', {static: false}) label: ElementRef;
@ViewChild('image', {static: false}) imageRef: ElementRef;
@Input('app-node') node: MapNode;
@Input('symbols') symbols: Symbol[];

View File

@ -5,10 +5,18 @@ import { DrawingsEventSource } from '../../events/drawings-event-source';
import { ToolsService } from '../../../services/tools.service';
import { Context } from '../../models/context';
import { Renderer2 } from '@angular/core';
import { MapScaleService } from '../../../services/mapScale.service';
import { LinkService } from '../../../services/link.service';
import { NodesDataSource } from '../../datasources/nodes-datasource';
import { LinksDataSource } from '../../datasources/links-datasource';
import { SelectionManager } from '../../managers/selection-manager';
import { FontFixer } from '../../helpers/font-fixer';
import { MockedLinkService } from '../../../components/project-map/project-map.component.spec';
describe('TemporaryTextElementComponent', () => {
describe('TextEditorComponent', () => {
let component: TextEditorComponent;
let fixture: ComponentFixture<TextEditorComponent>;
let mockedLinkService: MockedLinkService = new MockedLinkService();
beforeEach(async(() => {
TestBed.configureTestingModule({
@ -17,7 +25,13 @@ describe('TemporaryTextElementComponent', () => {
{ provide: DrawingsEventSource, useClass: DrawingsEventSource },
{ provide: ToolsService, useClass: ToolsService },
{ provide: Context, useClass: Context },
{ provide: Renderer2, useClass: Renderer2 }
{ provide: Renderer2, useClass: Renderer2 },
{ provide: MapScaleService, useClass: MapScaleService },
{ provide: LinkService, useValue: mockedLinkService },
{ provide: NodesDataSource, useClass: NodesDataSource },
{ provide: LinksDataSource, useClass: LinksDataSource },
{ provide: SelectionManager, useClass: SelectionManager },
{ provide: FontFixer, useClass: FontFixer }
],
declarations: [TextEditorComponent]
}).compileComponents();

View File

@ -6,6 +6,20 @@ import { select } from 'd3-selection';
import { TextElement } from '../../models/drawings/text-element';
import { Context } from '../../models/context';
import { Subscription } from 'rxjs';
import { MapScaleService } from '../../../services/mapScale.service';
import { MapLabel } from '../../models/map/map-label';
import { MapNode } from '../../models/map/map-node';
import { NodesDataSource } from '../../datasources/nodes-datasource';
import { Node } from '../../models/node';
import { SelectionManager } from '../../managers/selection-manager';
import { Server } from '../../../models/server';
import { MapLinkNode } from '../../models/map/map-link-node';
import { LinkService } from '../../../services/link.service';
import { LinksDataSource } from '../../datasources/links-datasource';
import { Link } from '../../../models/link';
import { StyleProperty } from '../../../components/project-map/drawings-editors/text-editor/text-editor.component';
import { FontFixer } from '../../helpers/font-fixer';
import { Font } from '../../models/font';
@Component({
selector: 'app-text-editor',
@ -13,8 +27,9 @@ import { Subscription } from 'rxjs';
styleUrls: ['./text-editor.component.scss']
})
export class TextEditorComponent implements OnInit, OnDestroy {
@ViewChild('temporaryTextElement') temporaryTextElement: ElementRef;
@ViewChild('temporaryTextElement', {static: false}) temporaryTextElement: ElementRef;
@Input('svg') svg: SVGSVGElement;
@Input('server') server: Server;
leftPosition: string = '0px';
topPosition: string = '0px';
@ -22,6 +37,8 @@ export class TextEditorComponent implements OnInit, OnDestroy {
private editingDrawingId: string;
private editedElement: any;
private editedLink: MapLinkNode;
private editedNode: Node;
private mapListener: Function;
private textListener: Function;
@ -32,7 +49,13 @@ export class TextEditorComponent implements OnInit, OnDestroy {
private drawingsEventSource: DrawingsEventSource,
private toolsService: ToolsService,
private context: Context,
private renderer: Renderer2
private renderer: Renderer2,
private mapScaleService: MapScaleService,
private linkService: LinkService,
private linksDataSource: LinksDataSource,
private nodesDataSource: NodesDataSource,
private selectionManager: SelectionManager,
private fontFixer: FontFixer
) {}
ngOnInit() {
@ -40,7 +63,8 @@ export class TextEditorComponent implements OnInit, OnDestroy {
isActive ? this.activateTextAdding() : this.deactivateTextAdding();
});
this.activateTextEditing();
this.activateTextEditingForDrawings();
this.activateTextEditingForNodeLabels();
}
activateTextAdding() {
@ -48,14 +72,15 @@ export class TextEditorComponent implements OnInit, OnDestroy {
this.leftPosition = event.pageX.toString() + 'px';
this.topPosition = event.pageY.toString() + 'px';
this.renderer.setStyle(this.temporaryTextElement.nativeElement, 'display', 'initial');
this.renderer.setStyle(this.temporaryTextElement.nativeElement, 'transform', `scale(${this.mapScaleService.getScale()})`);
this.temporaryTextElement.nativeElement.focus();
let textListener = () => {
this.drawingsEventSource.textAdded.emit(
new TextAddedDataEvent(
this.temporaryTextElement.nativeElement.innerText.replace(/\n$/, ''),
event.pageX - this.context.transformation.x,
event.pageY - this.context.transformation.y
event.pageX,
event.pageY
)
);
this.deactivateTextAdding();
@ -77,23 +102,90 @@ export class TextEditorComponent implements OnInit, OnDestroy {
this.svg.removeEventListener('click', this.mapListener as EventListenerOrEventListenerObject);
}
activateTextEditing() {
activateTextEditingForNodeLabels() {
const rootElement = select(this.svg);
rootElement
.selectAll<SVGGElement, MapLinkNode>('g.interface_label_container')
.select<SVGTextElement>('text.interface_label')
.on('dblclick', (elem, index, textElements) => {
this.selectionManager.setSelected([]);
this.renderer.setStyle(this.temporaryTextElement.nativeElement, 'display', 'initial');
this.renderer.setStyle(this.temporaryTextElement.nativeElement, 'transform', `scale(${this.mapScaleService.getScale()})`);
this.editedLink = elem;
select(textElements[index]).attr('visibility', 'hidden');
select(textElements[index]).classed('editingMode', true);
this.editedNode = this.nodesDataSource.get(elem.nodeId);
this.editedLink = elem;
let x = ((elem.label.originalX + this.editedNode.x - 1) * this.context.transformation.k) + this.context.getZeroZeroTransformationPoint().x + this.context.transformation.x;
let y = ((elem.label.originalY + this.editedNode.y + 4) * this.context.transformation.k) + this.context.getZeroZeroTransformationPoint().y + this.context.transformation.y;
this.leftPosition = x.toString() + 'px';
this.topPosition = y.toString() + 'px';
this.temporaryTextElement.nativeElement.innerText = elem.label.text;
let styleProperties: StyleProperty[] = [];
for (let property of elem.label.style.split(";")){
styleProperties.push({
property: property.split(": ")[0],
value: property.split(": ")[1]
});
}
let font: Font = {
font_family: styleProperties.find(p => p.property === 'font-family') ? styleProperties.find(p => p.property === 'font-family').value : 'TypeWriter',
font_size: styleProperties.find(p => p.property === 'font-size') ? Number(styleProperties.find(p => p.property === 'font-size').value) : 10.0,
font_weight: styleProperties.find(p => p.property === 'font-weight') ? styleProperties.find(p => p.property === 'font-weight').value : 'normal'
};
font = this.fontFixer.fix(font);
this.renderer.setStyle(this.temporaryTextElement.nativeElement, 'color', styleProperties.find(p => p.property === 'fill') ? styleProperties.find(p => p.property === 'fill').value : '#000000');
this.renderer.setStyle(this.temporaryTextElement.nativeElement, 'font-family', font.font_family);
this.renderer.setStyle(this.temporaryTextElement.nativeElement, 'font-size', `${font.font_size}pt`);
this.renderer.setStyle(this.temporaryTextElement.nativeElement, 'font-weight', font.font_weight);
let listener = () => {
let innerText = this.temporaryTextElement.nativeElement.innerText;
let link: Link = this.linksDataSource.get(this.editedLink.linkId);
link.nodes.find(n => n.node_id === this.editedNode.node_id).label.text = innerText;
this.linkService.updateLink(this.server, link).subscribe((link: Link) => {
rootElement
.selectAll<SVGTextElement, TextElement>('text.editingMode')
.attr('visibility', 'visible')
.classed('editingMode', false);
this.innerText = '';
this.temporaryTextElement.nativeElement.innerText = '';
this.temporaryTextElement.nativeElement.removeEventListener('focusout', this.textListener);
this.clearStyle();
this.renderer.setStyle(this.temporaryTextElement.nativeElement, 'display', 'none');
});
};
this.textListener = listener;
this.temporaryTextElement.nativeElement.addEventListener('focusout', this.textListener);
this.temporaryTextElement.nativeElement.focus();
});
}
activateTextEditingForDrawings() {
const rootElement = select(this.svg);
rootElement
.selectAll<SVGTextElement, TextElement>('text.text_element')
.on('dblclick', (elem, index, textElements) => {
this.renderer.setStyle(this.temporaryTextElement.nativeElement, 'display', 'initial');
this.renderer.setStyle(this.temporaryTextElement.nativeElement, 'transform', `scale(${this.mapScaleService.getScale()})`);
this.editedElement = elem;
select(textElements[index]).attr('visibility', 'hidden');
select(textElements[index]).classed('editingMode', true);
this.editingDrawingId = textElements[index].parentElement.parentElement.getAttribute('drawing_id');
var transformData = textElements[index].parentElement.getAttribute('transform').split(/\(|\)/);
var x = Number(transformData[1].split(/,/)[0]) + this.context.getZeroZeroTransformationPoint().x + this.context.transformation.x;
var y = Number(transformData[1].split(/,/)[1]) + this.context.getZeroZeroTransformationPoint().y + this.context.transformation.y;
var x = (Number(transformData[1].split(/,/)[0]) * this.context.transformation.k) + this.context.getZeroZeroTransformationPoint().x + this.context.transformation.x;
var y = (Number(transformData[1].split(/,/)[1]) * this.context.transformation.k) + this.context.getZeroZeroTransformationPoint().y + this.context.transformation.y;
this.leftPosition = x.toString() + 'px';
this.topPosition = y.toString() + 'px';
this.temporaryTextElement.nativeElement.innerText = elem.text;

View File

@ -0,0 +1,169 @@
import { ComponentFixture, TestBed, async, tick, fakeAsync } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { Context } from '../models/context';
import { MovingEventSource } from '../events/moving-event-source';
import { MovingCanvasDirective } from './moving-canvas.directive';
import { Component } from '@angular/core';
@Component({
template: `<svg #svg class="map" preserveAspectRatio="none" movingCanvas><g class="canvas" transform="translate(0, 0) scale(1)"></g></svg>`
})
class DummyComponent {
constructor(){}
}
describe('MovingCanvasDirective', () => {
let component: DummyComponent;
let fixture: ComponentFixture<DummyComponent>;
let movingEventSource = new MovingEventSource();
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [NoopAnimationsModule],
providers: [
{ provide: MovingEventSource, useValue: movingEventSource },
{ provide: Context, useClass: Context }
],
declarations: [DummyComponent, MovingCanvasDirective]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DummyComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should move canvas if moving mode is activated', fakeAsync(() => {
movingEventSource.movingModeState.emit(true);
const canvas: HTMLElement = fixture.debugElement.nativeElement.querySelector('.canvas');
let xMovement: number = 200;
let yMovement: number = 200;
canvas.dispatchEvent(new MouseEvent('mousedown', {
bubbles: true,
clientX: 0,
clientY: 0,
screenY: 0,
screenX: 0,
view: window
}));
tick();
canvas.dispatchEvent(new MouseEvent('mousemove', {
bubbles: true,
relatedTarget: canvas,
movementX: xMovement,
movementY: yMovement
} as MouseEventInit));
tick();
expect(canvas.getAttribute('transform')).toEqual(`translate(${xMovement}, ${yMovement}) scale(1)`);
}));
it('should not move canvas if moving mode is not activated', fakeAsync(() => {
const canvas: HTMLElement = fixture.debugElement.nativeElement.querySelector('.canvas');
canvas.dispatchEvent(new MouseEvent('mousedown', {
bubbles: true,
clientX: 0,
clientY: 0,
screenY: 0,
screenX: 0,
view: window
}));
tick();
canvas.dispatchEvent(new MouseEvent('mousemove', {
bubbles: true,
relatedTarget: canvas,
movementX: 1000,
movementY: 1000
} as MouseEventInit));
tick();
expect(canvas.getAttribute('transform')).toEqual('translate(0, 0) scale(1)');
}));
it('should not move canvas after mouseup event', fakeAsync(() => {
movingEventSource.movingModeState.emit(true);
const canvas: HTMLElement = fixture.debugElement.nativeElement.querySelector('.canvas');
let xMovement: number = 200;
let yMovement: number = 200;
canvas.dispatchEvent(new MouseEvent('mousedown', {
bubbles: true,
clientX: 0,
clientY: 0,
screenY: 0,
screenX: 0,
view: window
}));
tick();
canvas.dispatchEvent(new MouseEvent('mousemove', {
bubbles: true,
relatedTarget: canvas,
movementX: xMovement,
movementY: yMovement
} as MouseEventInit));
tick();
expect(canvas.getAttribute('transform')).toEqual(`translate(${xMovement}, ${yMovement}) scale(1)`);
canvas.dispatchEvent(new MouseEvent('mouseup', {
bubbles: true,
relatedTarget: canvas,
movementX: 1000,
movementY: 1000
} as MouseEventInit));
tick();
canvas.dispatchEvent(new MouseEvent('mousemove', {
bubbles: true,
relatedTarget: canvas,
movementX: xMovement,
movementY: yMovement
} as MouseEventInit));
tick();
expect(canvas.getAttribute('transform')).toEqual(`translate(${xMovement}, ${yMovement}) scale(1)`);
}));
it('should not move canvas after deactivation of moving mode', fakeAsync(() => {
movingEventSource.movingModeState.emit(true);
const canvas: HTMLElement = fixture.debugElement.nativeElement.querySelector('.canvas');
let xMovement: number = 200;
let yMovement: number = 200;
canvas.dispatchEvent(new MouseEvent('mousedown', {
bubbles: true,
clientX: 0,
clientY: 0,
screenY: 0,
screenX: 0,
view: window
}));
tick();
canvas.dispatchEvent(new MouseEvent('mousemove', {
bubbles: true,
relatedTarget: canvas,
movementX: xMovement,
movementY: yMovement
} as MouseEventInit));
tick();
expect(canvas.getAttribute('transform')).toEqual(`translate(${xMovement}, ${yMovement}) scale(1)`);
movingEventSource.movingModeState.emit(false);
canvas.dispatchEvent(new MouseEvent('mousemove', {
bubbles: true,
relatedTarget: canvas,
movementX: 1000,
movementY: 1000
} as MouseEventInit));
tick();
expect(canvas.getAttribute('transform')).toEqual(`translate(${xMovement}, ${yMovement}) scale(1)`);
}));
});

View File

@ -0,0 +1,65 @@
import { HostListener, ElementRef, Renderer, Directive, Input, OnInit, OnDestroy } from '@angular/core'
import { Subscription } from 'rxjs';
import { MovingEventSource } from '../events/moving-event-source';
import { Context } from '../models/context';
import { select } from 'd3-selection';
@Directive({
selector: '[movingCanvas]',
})
export class MovingCanvasDirective implements OnInit, OnDestroy {
private mouseupListener: Function;
private mousemoveListener: Function;
private movingModeState: Subscription;
private activated: boolean = false;
constructor(
private element: ElementRef,
private movingEventSource: MovingEventSource,
private context: Context
) {}
ngOnInit() {
this.movingModeState = this.movingEventSource.movingModeState.subscribe((event: boolean) => {
this.activated = event;
if (!event) this.removelisteners();
});
}
ngOnDestroy() {
this.movingModeState.unsubscribe();
}
@HostListener('mousedown', ['$event'])
onMouseDown(event: MouseEvent) {
if (this.activated) {
this.mousemoveListener = (event: MouseEvent) => {
const view = select(this.element.nativeElement);
const canvas = view.selectAll<SVGGElement, Context>('g.canvas').data([this.context]);
canvas.attr('transform', () => {
this.context.transformation.x = this.context.transformation.x + event.movementX;
this.context.transformation.y = this.context.transformation.y + event.movementY;
const xTrans = this.context.getZeroZeroTransformationPoint().x + this.context.transformation.x;
const yTrans = this.context.getZeroZeroTransformationPoint().y + this.context.transformation.y;
const kTrans = this.context.transformation.k;
return `translate(${xTrans}, ${yTrans}) scale(${kTrans})`;
});
};
this.mouseupListener = (event: MouseEvent) => {
this.removelisteners();
};
this.element.nativeElement.addEventListener('mouseup', this.mouseupListener as EventListenerOrEventListenerObject);
this.element.nativeElement.addEventListener('mousemove', this.mousemoveListener as EventListenerOrEventListenerObject);
}
}
removelisteners() {
this.element.nativeElement.removeEventListener('mouseup', this.mouseupListener as EventListenerOrEventListenerObject);
this.element.nativeElement.removeEventListener('mousemove', this.mousemoveListener as EventListenerOrEventListenerObject);
}
}

View File

@ -0,0 +1,139 @@
import { ComponentFixture, TestBed, async, tick, fakeAsync } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { Context } from '../models/context';
import { MovingEventSource } from '../events/moving-event-source';
import { Component } from '@angular/core';
import { ZoomingCanvasDirective } from './zooming-canvas.directive';
import { MapScaleService } from '../../services/mapScale.service';
@Component({
template: `<svg #svg class="map" preserveAspectRatio="none" zoomingCanvas><g class="canvas" transform="translate(0, 0) scale(1)"></g></svg>`
})
class DummyComponent {
constructor(){}
}
describe('ZoomingCanvasDirective', () => {
let component: DummyComponent;
let fixture: ComponentFixture<DummyComponent>;
let movingEventSource = new MovingEventSource();
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [NoopAnimationsModule],
providers: [
{ provide: MovingEventSource, useValue: movingEventSource },
{ provide: Context, useClass: Context },
{ provide: MapScaleService, useClass: MapScaleService }
],
declarations: [DummyComponent, ZoomingCanvasDirective]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DummyComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should zoom in canvas if moving mode is activated', fakeAsync(() => {
movingEventSource.movingModeState.emit(true);
const canvas: HTMLElement = fixture.debugElement.nativeElement.querySelector('.canvas');
let deltaMode: number = 0;
let zoom: number = -1000;
canvas.dispatchEvent(new WheelEvent('wheel', {
bubbles: true,
relatedTarget: canvas,
deltaMode: deltaMode,
deltaY: zoom
}));
tick();
expect(canvas.getAttribute('transform')).toEqual(`translate(0, 0) scale(2)`);
}));
it('should zoom out canvas if moving mode is activated', fakeAsync(() => {
movingEventSource.movingModeState.emit(true);
const canvas: HTMLElement = fixture.debugElement.nativeElement.querySelector('.canvas');
let deltaMode: number = 0;
let zoom: number = 100;
canvas.dispatchEvent(new WheelEvent('wheel', {
bubbles: true,
relatedTarget: canvas,
deltaMode: deltaMode,
deltaY: zoom
}));
tick();
expect(canvas.getAttribute('transform')).toEqual(`translate(0, 0) scale(0.9)`);
}));
it('should not zoom in/out canvas if moving mode is not activated', fakeAsync(() => {
movingEventSource.movingModeState.emit(true);
const canvas: HTMLElement = fixture.debugElement.nativeElement.querySelector('.canvas');
let deltaMode: number = 0;
let zoom: number = -1000;
canvas.dispatchEvent(new WheelEvent('wheel', {
bubbles: true,
relatedTarget: canvas,
deltaMode: deltaMode,
deltaY: zoom
}));
tick();
expect(canvas.getAttribute('transform')).toEqual(`translate(0, 0) scale(2)`);
movingEventSource.movingModeState.emit(false);
canvas.dispatchEvent(new WheelEvent('wheel', {
bubbles: true,
relatedTarget: canvas,
deltaMode: deltaMode,
deltaY: zoom
}));
tick();
expect(canvas.getAttribute('transform')).toEqual(`translate(0, 0) scale(2)`);
}));
it('should not zoom in/out canvas after deactivation of moving mode', fakeAsync(() => {
const canvas: HTMLElement = fixture.debugElement.nativeElement.querySelector('.canvas');
let deltaMode: number = 0;
let zoom: number = -1000;
canvas.dispatchEvent(new WheelEvent('wheel', {
bubbles: true,
relatedTarget: canvas,
deltaMode: deltaMode,
deltaY: zoom
}));
tick();
expect(canvas.getAttribute('transform')).toEqual(`translate(0, 0) scale(1)`);
}));
it('should prevent from default wheel behaviour when moving mode activated', fakeAsync(() => {
movingEventSource.movingModeState.emit(true);
const canvas: HTMLElement = fixture.debugElement.nativeElement.querySelector('.canvas');
let deltaMode: number = 0;
let zoom: number = -1000;
const event = new WheelEvent('wheel', {
bubbles: true,
relatedTarget: canvas,
deltaMode: deltaMode,
deltaY: zoom
});
spyOn(event, 'preventDefault');
canvas.dispatchEvent(event);
tick();
expect(event.preventDefault).toHaveBeenCalled();
}));
});

View File

@ -0,0 +1,61 @@
import { ElementRef, Directive, OnInit, OnDestroy } from '@angular/core'
import { Subscription } from 'rxjs';
import { MovingEventSource } from '../events/moving-event-source';
import { Context } from '../models/context';
import { select } from 'd3-selection';
import { MapScaleService } from '../../services/mapScale.service';
@Directive({
selector: '[zoomingCanvas]',
})
export class ZoomingCanvasDirective implements OnInit, OnDestroy {
private wheelListener: Function;
private movingModeState: Subscription;
constructor(
private element: ElementRef,
private movingEventSource: MovingEventSource,
private context: Context,
private mapsScaleService: MapScaleService
) {}
ngOnInit() {
this.movingModeState = this.movingEventSource.movingModeState.subscribe((event: boolean) => {
event ? this.addListener() : this.removeListener();
});
}
ngOnDestroy() {
this.movingModeState.unsubscribe();
}
addListener() {
this.wheelListener = (event: WheelEvent) => {
event.stopPropagation();
event.preventDefault();
let zoom = event.deltaY;
zoom = event.deltaMode === 0 ? zoom/100 : zoom/3;
const view = select(this.element.nativeElement);
const canvas = view.selectAll<SVGGElement, Context>('g.canvas').data([this.context]);
canvas.attr('transform', () => {
this.context.transformation.k = this.context.transformation.k - zoom/10;
const xTrans = this.context.getZeroZeroTransformationPoint().x + this.context.transformation.x;
const yTrans = this.context.getZeroZeroTransformationPoint().y + this.context.transformation.y;
const kTrans = this.context.transformation.k;
this.mapsScaleService.setScale(kTrans);
return `translate(${xTrans}, ${yTrans}) scale(${kTrans})`;
});
};
this.element.nativeElement.addEventListener('wheel', this.wheelListener as EventListenerOrEventListenerObject, {passive: false});
}
removeListener() {
this.element.nativeElement.removeEventListener('wheel', this.wheelListener as EventListenerOrEventListenerObject);
}
}

View File

@ -1,6 +1,8 @@
import { TextElement } from '../models/drawings/text-element';
import { MapDrawing } from '../models/map/map-drawing';
import { MapLink } from '../models/map/map-link';
import { MapLinkNode } from '../models/map/map-link-node';
import { MapLabel } from '../models/map/map-label';
export class DataEventSource<T> {
constructor(public datum: T, public dx: number, public dy: number) {}
@ -33,5 +35,13 @@ export class DrawingContextMenu {
}
export class LinkContextMenu {
constructor(public event:any, public link: MapLink) {}
constructor(public event: any, public link: MapLink) {}
}
export class InterfaceLabelContextMenu {
constructor(public event: any, public interfaceLabel: MapLinkNode) {}
}
export class LabelContextMenu {
constructor(public event: any, public label: MapLabel) {}
}

View File

@ -0,0 +1,6 @@
import { Injectable, EventEmitter } from "@angular/core";
@Injectable()
export class MovingEventSource {
public movingModeState = new EventEmitter<boolean>();
}

View File

@ -100,7 +100,9 @@ export class SelectionTool {
}
private moveSelection(start, move) {
this.path.attr('d', this.rect(start[0], start[1], move[0] - start[0], move[1] - start[1]));
let x = start[0]/this.context.transformation.k;
let y = start[1]/this.context.transformation.k;
this.path.attr('d', this.rect(x, y, move[0]/this.context.transformation.k - x, move[1]/this.context.transformation.k - y));
this.selectedEvent(start, move);
}

View File

@ -1,4 +1,4 @@
import { Injectable } from '@angular/core';
import { Injectable, EventEmitter } from '@angular/core';
import { SVGSelection } from '../models/types';
import { CssFixer } from '../helpers/css-fixer';
@ -10,9 +10,11 @@ import { MapLinkNode } from '../models/map/map-link-node';
import { MapNode } from '../models/map/map-node';
import { Draggable } from '../events/draggable';
import { MapSettingsManager } from '../managers/map-settings-manager';
import { InterfaceLabelContextMenu } from '../events/event-source';
@Injectable()
export class InterfaceLabelWidget {
public onContextMenu = new EventEmitter<InterfaceLabelContextMenu>();
public draggable = new Draggable<SVGGElement, MapLinkNode>();
static SURROUNDING_TEXT_BORDER = 5;
@ -30,6 +32,8 @@ export class InterfaceLabelWidget {
}
draw(selection: SVGSelection) {
const self = this;
const link_node_position = selection
.selectAll<SVGGElement, MapLinkNode>('g.link_node_position')
.data((link: MapLink) => [[link.source, link.nodes[0]], [link.target, link.nodes[1]]]);
@ -68,7 +72,12 @@ export class InterfaceLabelWidget {
.attr('class', 'interface_label noselect')
.attr('interface_label_id', (i: MapLinkNode) => `${i.id}`);
const merge = labels.merge(enter);
const merge = labels
.merge(enter)
.on('contextmenu', (n: MapLinkNode, i: number) => {
event.preventDefault();
self.onContextMenu.emit(new InterfaceLabelContextMenu(event, n));
});
// update label
merge

View File

@ -1,4 +1,4 @@
import { Injectable } from '@angular/core';
import { Injectable, EventEmitter } from '@angular/core';
import { Widget } from './widget';
import { SVGSelection } from '../models/types';
@ -10,9 +10,12 @@ import { SelectionManager } from '../managers/selection-manager';
import { Draggable } from '../events/draggable';
import { MapLabel } from '../models/map/map-label';
import { MapSettingsManager } from '../managers/map-settings-manager';
import { LabelContextMenu } from '../events/event-source';
import { TextElement } from '../models/drawings/text-element';
@Injectable()
export class LabelWidget implements Widget {
public onContextMenu = new EventEmitter<LabelContextMenu>();
public draggable = new Draggable<SVGGElement, MapLabel>();
static NODE_LABEL_MARGIN = 3;
@ -29,6 +32,7 @@ export class LabelWidget implements Widget {
}
public draw(view: SVGSelection) {
const self = this;
const label_view = view.selectAll<SVGGElement, MapLabel>('g.label_container').data((node: MapNode) => {
return [node.label];
});
@ -39,7 +43,12 @@ export class LabelWidget implements Widget {
.attr('class', 'label_container')
.attr('label_id', (label: MapLabel) => label.id);
const merge = label_view.merge(label_enter);
const merge = label_view
.merge(label_enter)
.on('contextmenu', (n: MapLabel, i: number) => {
event.preventDefault();
self.onContextMenu.emit(new LabelContextMenu(event, n));
});
this.drawLabel(merge);
@ -63,7 +72,8 @@ export class LabelWidget implements Widget {
label_body_enter.append<SVGRectElement>('rect').attr('class', 'label_selection');
const label_body_merge = label_body.merge(label_body_enter);
const label_body_merge = label_body
.merge(label_body_enter);
label_body_merge
.select<SVGTextElement>('text.label')
@ -82,7 +92,9 @@ export class LabelWidget implements Widget {
label_body_merge
.select<SVGRectElement>('rect.label_selection')
.attr('visibility', (l: MapLabel) => (this.selectionManager.isSelected(l) ? 'visible' : 'hidden'))
.attr('visibility', (l: MapLabel) => {
return (this.selectionManager.isSelected(l) ? 'visible' : 'hidden')
})
.attr('stroke', 'black')
.attr('stroke-dasharray', '3,3')
.attr('stroke-width', '0.5')

View File

@ -38,17 +38,17 @@ export class NodeWidget implements Widget {
const node_body_merge = node_body
.merge(node_body_enter)
.classed('selected', (n: MapNode) => this.selectionManager.isSelected(n))
.on('contextmenu', function(n: MapNode, i: number) {
event.preventDefault();
self.onContextMenu.emit(new NodeContextMenu(event, n));
})
.on('click', (node: MapNode) => {
this.nodesEventSource.clicked.emit(new ClickedDataEvent<MapNode>(node, event.clientX, event.clientY));
this.nodesEventSource.clicked.emit(new ClickedDataEvent<MapNode>(node, event.pageX, event.pageY));
});
// update image of node
node_body_merge
.select<SVGImageElement>('image')
.on('contextmenu', function(n: MapNode, i: number) {
event.preventDefault();
self.onContextMenu.emit(new NodeContextMenu(event, n));
})
.attr('xnode:href', (n: MapNode) => n.symbolUrl)
.attr('width', (n: MapNode) => n.width)
.attr('height', (n: MapNode) => n.height)

View File

@ -5,7 +5,7 @@ import { MatIconModule, MatProgressSpinnerModule } from '@angular/material';
import { ProgressService } from './progress.service';
import { RouterTestingModule } from '@angular/router/testing';
import { Router } from '@angular/router';
import { BehaviorSubject } from 'rxjs';
import { BehaviorSubject, Observable } from 'rxjs';
class MockedRouter {
events: BehaviorSubject<boolean>;
@ -13,18 +13,20 @@ class MockedRouter {
constructor() {
this.events = new BehaviorSubject(true);
}
navigateByUrl() {}
}
describe('ProgressComponent', () => {
let component: ProgressComponent;
let fixture: ComponentFixture<ProgressComponent>;
let progressService: ProgressService;
let router: MockedRouter;
let router: MockedRouter = new MockedRouter();
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [RouterTestingModule, MatProgressSpinnerModule, MatIconModule],
providers: [ProgressService, { provide: Router, useClass: MockedRouter }],
providers: [ProgressService, { provide: Router, useValue: router }],
declarations: [ProgressComponent]
}).compileComponents();
@ -71,4 +73,12 @@ describe('ProgressComponent', () => {
expect(progressService.clear).toHaveBeenCalled();
});
it("should reload page after clicking refresh", () => {
spyOn(router, 'navigateByUrl');
component.refresh();
expect(router.navigateByUrl).toHaveBeenCalled();
});
});

View File

@ -36,8 +36,7 @@ export class ProgressComponent implements OnInit, OnDestroy {
}
refresh() {
// unfortunately we need to use global var
location.reload();
this.router.navigateByUrl(this.router.url);
}
ngOnDestroy() {

View File

@ -9,9 +9,8 @@ import { AdButlerResponse } from '../../models/adbutler';
encapsulation: ViewEncapsulation.None
})
export class AdbutlerComponent implements OnInit {
@ViewChild('ad') ad: ElementRef;
@ViewChild('ad', {static: false}) ad: ElementRef;
@Input() theme: string;
htmlCode: string;
constructor(

View File

@ -35,12 +35,48 @@ export class LinkCreatedComponent implements OnInit, OnDestroy {
}
onLinkCreated(linkCreated: MapLinkCreated) {
const xLength = Math.abs(linkCreated.sourceNode.x - linkCreated.targetNode.x);
const yLength = Math.abs(linkCreated.sourceNode.y - linkCreated.targetNode.y);
const zLength = Math.sqrt(Math.pow(xLength, 2) + Math.pow(yLength, 2));
//from law of sines
const sinY = yLength/zLength;
const x = (45 / zLength) * xLength;
const y = (45 / zLength) * yLength;
let xLabelSourceNode = 0;
let yLabelSourceNode = 0;
let xLabelTargetNode = 0;
let yLabelTargetNode = 0;
if ((linkCreated.sourceNode.x <= linkCreated.targetNode.x) && (linkCreated.sourceNode.y <= linkCreated.targetNode.y)) {
xLabelSourceNode = Math.floor(linkCreated.sourceNode.width/2) + Math.round(x) + 5;
yLabelSourceNode = Math.floor(linkCreated.sourceNode.height/2) + Math.round(y) + 5;
xLabelTargetNode = Math.floor(linkCreated.targetNode.width/2) - Math.round(x) - 5 - Math.round(20 * sinY);
yLabelTargetNode = Math.floor(linkCreated.targetNode.height/2) - Math.round(y) + 5 - Math.round(20 * sinY);
} else if ((linkCreated.sourceNode.x > linkCreated.targetNode.x) && (linkCreated.sourceNode.y < linkCreated.targetNode.y)) {
xLabelSourceNode = Math.floor(linkCreated.sourceNode.width/2) - Math.round(x) - 5 - Math.round(20 * sinY);
yLabelSourceNode = Math.floor(linkCreated.sourceNode.height/2) + Math.round(y) + 5 - Math.round(20 * sinY);
xLabelTargetNode = Math.floor(linkCreated.targetNode.width/2) + Math.round(x) + 5;
yLabelTargetNode = Math.floor(linkCreated.targetNode.height/2) - Math.round(y) - 5;
} else if ((linkCreated.sourceNode.x < linkCreated.targetNode.x) && (linkCreated.sourceNode.y > linkCreated.targetNode.y)) {
xLabelSourceNode = Math.floor(linkCreated.sourceNode.width/2) + Math.round(x) + 5 - Math.round(20 * sinY);
yLabelSourceNode = Math.floor(linkCreated.sourceNode.height/2) - Math.round(y) - 5 - Math.round(20 * sinY);
xLabelTargetNode = Math.floor(linkCreated.targetNode.width/2) - Math.round(x) - 5;
yLabelTargetNode = Math.floor(linkCreated.targetNode.height/2) + Math.round(y) + 5;
} else if ((linkCreated.sourceNode.x >= linkCreated.targetNode.x) && (linkCreated.sourceNode.y >= linkCreated.targetNode.y)) {
xLabelSourceNode = Math.floor(linkCreated.sourceNode.width/2) - Math.round(x) - 5 - Math.round(20 * sinY);
yLabelSourceNode = Math.floor(linkCreated.sourceNode.height/2) - Math.round(y) + 5 - Math.round(20 * sinY);
xLabelTargetNode = Math.floor(linkCreated.targetNode.width/2) + Math.round(x) + 5;
yLabelTargetNode = Math.floor(linkCreated.targetNode.height/2) + Math.round(y) + 5;
}
const sourceNode = this.mapNodeToNode.convert(linkCreated.sourceNode);
const sourcePort = this.mapPortToPort.convert(linkCreated.sourcePort);
const targetNode = this.mapNodeToNode.convert(linkCreated.targetNode);
const targetPort = this.mapPortToPort.convert(linkCreated.targetPort);
this.linkService.createLink(this.server, sourceNode, sourcePort, targetNode, targetPort).subscribe(() => {
this.linkService.createLink(this.server, sourceNode, sourcePort, targetNode, targetPort, xLabelSourceNode, yLabelSourceNode, xLabelTargetNode, yLabelTargetNode).subscribe(() => {
this.projectService.links(this.server, this.project.project_id).subscribe((links: Link[]) => {
this.linksDataSource.set(links);
});

View File

@ -45,8 +45,8 @@ export class TextAddedComponent implements OnInit, OnDestroy {
.add(
this.server,
this.project.project_id,
evt.x - this.context.getZeroZeroTransformationPoint().x,
evt.y - this.context.getZeroZeroTransformationPoint().y,
(evt.x - (this.context.getZeroZeroTransformationPoint().x + this.context.transformation.x))/this.context.transformation.k,
(evt.y - (this.context.getZeroZeroTransformationPoint().y + this.context.transformation.y))/this.context.transformation.k,
svgText
)
.subscribe((serverDrawing: Drawing) => {

View File

@ -0,0 +1,21 @@
<div class="content">
<div class="default-header"><h1>Help</h1></div>
<div class="default-content">
<div class="container mat-elevation-z8">
<mat-accordion>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title> Third party components </mat-panel-title>
</mat-expansion-panel-header>
<div [innerHTML]="thirdpartylicenses"></div>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title> Release notes </mat-panel-title>
</mat-expansion-panel-header>
<div [innerHTML]="releasenotes"></div>
</mat-expansion-panel>
</mat-accordion>
</div>
</div>
</div>

View File

@ -0,0 +1,33 @@
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-help',
templateUrl: './help.component.html',
styleUrls: ['./help.component.scss']
})
export class HelpComponent implements OnInit {
thirdpartylicenses = '';
releasenotes = '';
constructor(
private httpClient: HttpClient
) {}
ngOnInit() {
this.httpClient.get(window.location.href + '/3rdpartylicenses.txt', {responseType: 'text'})
.subscribe(data => {
this.thirdpartylicenses = data.replace(new RegExp('\n', 'g'), "<br />")
},
error => {
if (error.status === 404) {
this.thirdpartylicenses = 'File not found';
}
});
this.httpClient.get('ReleaseNotes.txt', {responseType: 'text'})
.subscribe(data => {
this.releasenotes = data.replace(new RegExp('\n', 'g'), "<br />")
});
}
}

View File

@ -8,7 +8,7 @@
<mat-card class="matCard">
<form [formGroup]="formGroup">
<mat-form-field class="form-field">
<input formControlName="templateName" matInput type="text" [(ngModel)]="templateName" placeholder="Template name">
<input formControlName="templateName" matInput type="text" placeholder="Template name">
</mat-form-field>
</form>
</mat-card>

View File

@ -54,7 +54,7 @@ export class CloudNodesAddTemplateComponent implements OnInit {
});
cloudTemplate.template_id = uuid();
cloudTemplate.name = this.templateName;
cloudTemplate.name = this.formGroup.get('templateName').value;
this.builtInTemplatesService.addTemplate(this.server, cloudTemplate).subscribe((cloudNodeTemplate) => {
this.goBack();

View File

@ -15,7 +15,7 @@ import { DeleteTemplateComponent } from '../../../common/delete-template-compone
export class CloudNodesTemplatesComponent implements OnInit {
server: Server;
cloudNodesTemplates: CloudTemplate[] = [];
@ViewChild(DeleteTemplateComponent) deleteComponent: DeleteTemplateComponent;
@ViewChild(DeleteTemplateComponent, {static: false}) deleteComponent: DeleteTemplateComponent;
constructor(
private route: ActivatedRoute,

View File

@ -8,10 +8,10 @@
<mat-card class="matCard">
<form [formGroup]="formGroup">
<mat-form-field class="form-field">
<input formControlName="templateName" matInput type="text" [(ngModel)]="templateName" placeholder="Template name">
<input formControlName="templateName" matInput type="text" placeholder="Template name">
</mat-form-field>
<mat-form-field class="form-field">
<input formControlName="numberOfPorts" matInput type="text" [(ngModel)]="numberOfPorts" placeholder="Number of ports">
<input formControlName="numberOfPorts" matInput type="text" placeholder="Number of ports">
</mat-form-field>
</form>
</mat-card>

View File

@ -75,7 +75,6 @@ describe('EthernetHubsAddTemplateComponent', () => {
it('should call add template', () => {
spyOn(mockedBuiltInTemplatesService, 'addTemplate').and.returnValue(of({} as EthernetHubTemplate));
component.templateName = "sample name";
component.numberOfPorts = 2;
component.server = {id: 1} as Server;
component.formGroup.controls['templateName'].setValue('template name');
component.formGroup.controls['numberOfPorts'].setValue('1');
@ -89,7 +88,6 @@ describe('EthernetHubsAddTemplateComponent', () => {
spyOn(mockedBuiltInTemplatesService, 'addTemplate').and.returnValue(of({} as EthernetHubTemplate));
spyOn(mockedToasterService, 'error');
component.templateName = "";
component.numberOfPorts = 2;
component.server = {id: 1} as Server;
component.addTemplate();

View File

@ -17,7 +17,6 @@ import { FormBuilder, FormGroup, FormControl, Validators } from '@angular/forms'
})
export class EthernetHubsAddTemplateComponent implements OnInit {
server: Server;
numberOfPorts: number;
templateName: string = '';
formGroup: FormGroup;
@ -56,9 +55,9 @@ export class EthernetHubsAddTemplateComponent implements OnInit {
});
ethernetHubTemplate.template_id = uuid();
ethernetHubTemplate.name = this.templateName;
ethernetHubTemplate.name = this.formGroup.get('templateName').value;
for(let i=0; i<this.numberOfPorts; i++){
for(let i=0; i<this.formGroup.get('numberOfPorts').value; i++){
ethernetHubTemplate.ports_mapping.push({
name: `Ethernet${i}`,
port_number: i

View File

@ -15,7 +15,7 @@ import { DeleteTemplateComponent } from '../../../common/delete-template-compone
export class EthernetHubsTemplatesComponent implements OnInit {
server: Server;
ethernetHubsTemplates: EthernetHubTemplate[] = [];
@ViewChild(DeleteTemplateComponent) deleteComponent: DeleteTemplateComponent;
@ViewChild(DeleteTemplateComponent, {static: false}) deleteComponent: DeleteTemplateComponent;
constructor(
private route: ActivatedRoute,

View File

@ -8,7 +8,10 @@
<mat-card class="matCard">
<form [formGroup]="formGroup">
<mat-form-field class="form-field">
<input formControlName="templateName" matInput type="text" [(ngModel)]="templateName" placeholder="Template name">
<input formControlName="templateName" matInput type="text" placeholder="Template name">
</mat-form-field>
<mat-form-field class="form-field">
<input formControlName="numberOfPorts" matInput type="text" placeholder="Number of ports">
</mat-form-field>
</form>
</mat-card>

View File

@ -75,7 +75,6 @@ describe('EthernetSwitchesAddTemplateComponent', () => {
it('should call add template', () => {
spyOn(mockedBuiltInTemplatesService, 'addTemplate').and.returnValue(of({} as EthernetSwitchTemplate));
component.templateName = "sample name";
component.numberOfPorts = 2;
component.server = {id: 1} as Server;
component.formGroup.controls['templateName'].setValue('template name');
component.formGroup.controls['numberOfPorts'].setValue('1');

View File

@ -17,7 +17,6 @@ import { FormGroup, FormBuilder, FormControl, Validators } from '@angular/forms'
})
export class EthernetSwitchesAddTemplateComponent implements OnInit {
server: Server;
numberOfPorts: number;
templateName: string = '';
formGroup: FormGroup;
@ -56,9 +55,9 @@ export class EthernetSwitchesAddTemplateComponent implements OnInit {
});
ethernetSwitchTemplate.template_id = uuid();
ethernetSwitchTemplate.name = this.templateName;
ethernetSwitchTemplate.name = this.formGroup.get('templateName').value;
for(let i=0; i<this.numberOfPorts; i++){
for(let i=0; i<this.formGroup.get('numberOfPorts').value; i++){
ethernetSwitchTemplate.ports_mapping.push({
ethertype: '',
name: `Ethernet${i}`,

View File

@ -15,7 +15,7 @@ import { DeleteTemplateComponent } from '../../../common/delete-template-compone
export class EthernetSwitchesTemplatesComponent implements OnInit {
server: Server;
ethernetSwitchesTemplates: EthernetSwitchTemplate[] = [];
@ViewChild(DeleteTemplateComponent) deleteComponent: DeleteTemplateComponent;
@ViewChild(DeleteTemplateComponent, {static: false}) deleteComponent: DeleteTemplateComponent;
constructor(
private route: ActivatedRoute,

View File

@ -34,7 +34,6 @@
matInput
class="filename"
type="text"
[(ngModel)]="dockerTemplate.image"
formControlName="filename"
placeholder="Image name"/>
</mat-form-field>
@ -48,7 +47,6 @@
matInput
class="templatename"
type="text"
[(ngModel)]="dockerTemplate.name"
formControlName="templateName"
placeholder="Container name"/>
</mat-form-field>
@ -61,7 +59,6 @@
matInput
class="networkadapter"
type="number"
[(ngModel)]="dockerTemplate.adapters"
formControlName="adapters"
placeholder="Adapters"/>
</mat-form-field>

View File

@ -87,6 +87,9 @@ export class AddDockerTemplateComponent implements OnInit {
addTemplate() {
if ((!this.virtualMachineForm.invalid || !this.newImageSelected) && !this.containerNameForm.invalid && !this.networkAdaptersForm.invalid) {
this.dockerTemplate.template_id = uuid();
this.dockerTemplate.image = this.virtualMachineForm.get('filename').value;
this.dockerTemplate.name = this.containerNameForm.get('templateName').value;
this.dockerTemplate.adapters = this.networkAdaptersForm.get('adapters').value;
this.dockerService.addTemplate(this.server, this.dockerTemplate).subscribe((template: DockerTemplate) => {
this.goBack();

View File

@ -15,7 +15,7 @@ import { DockerService } from '../../../../services/docker.service';
export class DockerTemplatesComponent implements OnInit {
server: Server;
dockerTemplates: DockerTemplate[] = [];
@ViewChild(DeleteTemplateComponent) deleteComponent: DeleteTemplateComponent;
@ViewChild(DeleteTemplateComponent, {static: false}) deleteComponent: DeleteTemplateComponent;
constructor(
private route: ActivatedRoute,

View File

@ -12,7 +12,6 @@
<mat-form-field class="form-field">
<input
matInput type="text"
[(ngModel)]="iosTemplate.image"
formControlName="imageName"
placeholder="IOS image"/>
</mat-form-field>
@ -23,7 +22,6 @@
<mat-form-field class="form-field">
<input
matInput type="text"
[(ngModel)]="iosTemplate.name"
formControlName="templateName"
placeholder="Name"/>
</mat-form-field>
@ -31,8 +29,7 @@
<mat-select
placeholder="Platform"
(selectionChange)="onPlatformChosen($event)"
formControlName="platform"
[(ngModel)]="iosTemplate.platform">
formControlName="platform">
<mat-option *ngFor="let platform of platforms" [value]="platform">
{{platform}}
</mat-option>
@ -42,8 +39,7 @@
<mat-select
placeholder="Chassis"
(selectionChange)="onChassisChosen($event)"
formControlName="chassis"
[(ngModel)]="iosTemplate.chassis">
formControlName="chassis">
<mat-option *ngFor="let chassis of chassis[iosTemplate.platform]" [value]="chassis">
{{chassis}}
</mat-option>
@ -61,7 +57,6 @@
<mat-form-field class="form-field">
<input
matInput type="number"
[(ngModel)]="iosTemplate.ram"
formControlName="memory"
value="defaultRam[iosTemplate.platform]"
placeholder="Default RAM"/>

View File

@ -90,6 +90,11 @@ export class AddIosTemplateComponent implements OnInit {
addTemplate() {
if (!this.iosImageForm.invalid && !this.iosNameForm.invalid && !this.iosMemoryForm.invalid) {
this.iosTemplate.template_id = uuid();
this.iosTemplate.image = this.iosImageForm.get("imageName").value;
this.iosTemplate.name = this.iosNameForm.get('templateName').value;
this.iosTemplate.platform = this.iosNameForm.get('platform').value;
this.iosTemplate.chassis = this.iosNameForm.get('chassis').value;
this.iosTemplate.ram = this.iosMemoryForm.get('memory').value;
if (this.isEtherSwitchRouter) {
this.iosTemplate.symbol = ":/symbols/multilayer_switch.svg";

View File

@ -16,7 +16,7 @@ import { VpcsTemplate } from '../../../../models/templates/vpcs-template';
export class IosTemplatesComponent implements OnInit {
server: Server;
iosTemplates: IosTemplate[] = [];
@ViewChild(DeleteTemplateComponent) deleteComponent: DeleteTemplateComponent;
@ViewChild(DeleteTemplateComponent, {static: false}) deleteComponent: DeleteTemplateComponent;
constructor(
private route: ActivatedRoute,

View File

@ -16,7 +16,7 @@
<mat-step label="Name">
<form [formGroup]="templateNameForm">
<mat-form-field class="form-field">
<input matInput placeholder="Name" type="text" [(ngModel)]="iouTemplate.name" formControlName="templateName"/>
<input matInput placeholder="Name" type="text" formControlName="templateName"/>
</mat-form-field>
</form>
</mat-step>
@ -59,7 +59,6 @@
<input
matInput
type="text"
[(ngModel)]="iouTemplate.path"
formControlName="imageName"
placeholder="IOU image"/>
</mat-form-field>

View File

@ -82,6 +82,8 @@ export class AddIouTemplateComponent implements OnInit {
addTemplate() {
if (!this.templateNameForm.invalid && ((this.newImageSelected && !this.imageForm.invalid) || (!this.newImageSelected && this.iouTemplate.path))) {
this.iouTemplate.template_id = uuid();
this.iouTemplate.name = this.templateNameForm.get("templateName").value;
this.iouTemplate.path = this.imageForm.get("imageName").value;
this.iouService.addTemplate(this.server, this.iouTemplate).subscribe((template: IouTemplate) => {
this.goBack();

View File

@ -15,7 +15,7 @@ import { IouService } from '../../../../services/iou.service';
export class IouTemplatesComponent implements OnInit {
server: Server;
iouTemplates: IouTemplate[] = [];
@ViewChild(DeleteTemplateComponent) deleteComponent: DeleteTemplateComponent;
@ViewChild(DeleteTemplateComponent, {static: false}) deleteComponent: DeleteTemplateComponent;
constructor(
private route: ActivatedRoute,

View File

@ -12,7 +12,6 @@
<mat-form-field class="form-field">
<input
matInput type="text"
[(ngModel)]="qemuTemplate.name"
formControlName="templateName"
placeholder="Please choose a descriptive name for your new QEMU virtual machine"
ngDefaultContro/>
@ -26,9 +25,9 @@
<form [formGroup]="memoryForm">
<mat-form-field class="form-field">
<mat-select
placeholder="Qemu binary"
placeholder="Qemu binary"
[(ngModel)]="selectedBinary"
formControlName="binary" >
[ngModelOptions]="{standalone: true}">
<mat-option *ngFor="let binary of qemuBinaries" [value]="binary">
{{binary.path}}
</mat-option>
@ -38,7 +37,6 @@
<input
matInput type="number"
placeholder="RAM"
[(ngModel)]="ramMemory"
formControlName="ramMemory"
ngDefaultContro/>
</mat-form-field>

View File

@ -51,7 +51,6 @@ export class AddQemuVmTemplateComponent implements OnInit {
});
this.memoryForm = this.formBuilder.group({
binary: new FormControl('', Validators.required),
ramMemory: new FormControl('', Validators.required)
});
@ -95,7 +94,7 @@ export class AddQemuVmTemplateComponent implements OnInit {
addTemplate() {
if (!this.nameForm.invalid && !this.memoryForm.invalid && (this.selectedImage || this.chosenImage)) {
this.qemuTemplate.ram = this.ramMemory;
this.qemuTemplate.ram = this.memoryForm.get("ramMemory").value;
this.qemuTemplate.qemu_path = this.selectedBinary.path;
if (this.newImageSelected) {
this.qemuTemplate.hda_disk_image = this.chosenImage;
@ -103,6 +102,7 @@ export class AddQemuVmTemplateComponent implements OnInit {
this.qemuTemplate.hda_disk_image = this.selectedImage.path;
}
this.qemuTemplate.template_id = uuid();
this.qemuTemplate.name = this.nameForm.get("templateName").value;
this.qemuService.addTemplate(this.server, this.qemuTemplate).subscribe((template: QemuTemplate) => {
this.goBack();

View File

@ -34,7 +34,7 @@ export class QemuVmTemplateDetailsComponent implements OnInit {
displayedColumns: string[] = ['adapter_number', 'port_name', 'adapter_type'];
generalSettingsForm: FormGroup;
@ViewChild("customAdaptersConfigurator")
@ViewChild("customAdaptersConfigurator", {static: false})
customAdaptersConfigurator: CustomAdaptersComponent;
constructor(

View File

@ -15,7 +15,7 @@ import { DeleteTemplateComponent } from '../../common/delete-template-component/
export class QemuVmTemplatesComponent implements OnInit {
server: Server;
qemuTemplates: QemuTemplate[] = [];
@ViewChild(DeleteTemplateComponent) deleteComponent: DeleteTemplateComponent;
@ViewChild(DeleteTemplateComponent, {static: false}) deleteComponent: DeleteTemplateComponent;
constructor(
private route: ActivatedRoute,

View File

@ -29,7 +29,7 @@ export class VirtualBoxTemplateDetailsComponent implements OnInit {
generalSettingsForm: FormGroup;
networkForm: FormGroup
@ViewChild("customAdaptersConfigurator")
@ViewChild("customAdaptersConfigurator", {static: false})
customAdaptersConfigurator: CustomAdaptersComponent;
constructor(

View File

@ -16,7 +16,7 @@ import { VpcsTemplate } from '../../../../models/templates/vpcs-template';
export class VirtualBoxTemplatesComponent implements OnInit {
server: Server;
virtualBoxTemplates: VirtualBoxTemplate[] = [];
@ViewChild(DeleteTemplateComponent) deleteComponent: DeleteTemplateComponent;
@ViewChild(DeleteTemplateComponent, {static: false}) deleteComponent: DeleteTemplateComponent;
constructor(
private route: ActivatedRoute,

View File

@ -28,7 +28,7 @@ export class VmwareTemplateDetailsComponent implements OnInit {
onCloseOptions = [];
networkTypes = [];
@ViewChild("customAdaptersConfigurator")
@ViewChild("customAdaptersConfigurator", {static: false})
customAdaptersConfigurator: CustomAdaptersComponent;
constructor(

View File

@ -15,7 +15,7 @@ import { DeleteTemplateComponent } from '../../common/delete-template-component/
export class VmwareTemplatesComponent implements OnInit {
server: Server;
vmwareTemplates: VmwareTemplate[] = [];
@ViewChild(DeleteTemplateComponent) deleteComponent: DeleteTemplateComponent;
@ViewChild(DeleteTemplateComponent, {static: false}) deleteComponent: DeleteTemplateComponent;
constructor(
private route: ActivatedRoute,

View File

@ -14,7 +14,7 @@ import { DeleteTemplateComponent } from '../../common/delete-template-component/
export class VpcsTemplatesComponent implements OnInit {
server: Server;
vpcsTemplates: VpcsTemplate[] = [];
@ViewChild(DeleteTemplateComponent) deleteComponent: DeleteTemplateComponent;
@ViewChild(DeleteTemplateComponent, {static: false}) deleteComponent: DeleteTemplateComponent;
constructor(
private route: ActivatedRoute,

View File

@ -103,10 +103,10 @@ describe('ConsoleDeviceActionComponent', () => {
});
});
it('should show message when command is not defined', async () => {
it('should set command when it is not defined', async () => {
mockedSettingsService.set('console_command', undefined);
await component.console();
expect(component.openConsole).not.toHaveBeenCalled();
expect(component.openConsole).toHaveBeenCalled();
});
it('should show message when there is no started nodes', async () => {

View File

@ -25,11 +25,10 @@ export class ConsoleDeviceActionComponent implements OnInit {
}
async console() {
const consoleCommand = this.settingsService.get<string>('console_command');
let consoleCommand = this.settingsService.get<string>('console_command');
if(consoleCommand === undefined) {
this.toasterService.error('Console command is not defined. Please change it in the Settings.');
return;
consoleCommand = `putty.exe -telnet \%h \%p -wt \"\%d\" -gns3 5 -skin 4`;
}
const startedNodes = this.nodes.filter(node => node.status === 'started');

View File

@ -0,0 +1,4 @@
<button mat-menu-item (click)="duplicate()">
<mat-icon>filter_none</mat-icon>
<span>Duplicate</span>
</button>

View File

@ -0,0 +1,82 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MatIconModule, MatMenuModule } from '@angular/material';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { NodesDataSource } from '../../../../../cartography/datasources/nodes-datasource';
import { DrawingsDataSource } from '../../../../../cartography/datasources/drawings-datasource';
import { NodeService } from '../../../../../services/node.service';
import { DrawingService } from '../../../../../services/drawing.service';
import { MockedDrawingService, MockedNodeService } from '../../../project-map.component.spec';
import { Node } from '../../../../../cartography/models/node';
import { Drawing } from '../../../../../cartography/models/drawing';
import { of } from 'rxjs';
import { DuplicateActionComponent } from './duplicate-action.component';
import { ToasterService } from '../../../../../services/toaster.service';
import { MockedToasterService } from '../../../../../services/toaster.service.spec';
describe('DuplicateActionComponent', () => {
let component: DuplicateActionComponent;
let fixture: ComponentFixture<DuplicateActionComponent>;
let mockedNodeService: MockedNodeService = new MockedNodeService();
let mockedDrawingService: MockedDrawingService = new MockedDrawingService();
let mockedToasterService = new MockedToasterService;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [MatIconModule, MatMenuModule, NoopAnimationsModule],
providers: [
{ provide: NodesDataSource, useClass: NodesDataSource },
{ provide: DrawingsDataSource, useClass: DrawingsDataSource },
{ provide: NodeService, useValue: mockedNodeService },
{ provide: DrawingService, useValue: mockedDrawingService },
{ provide: ToasterService, useValue: mockedToasterService }
],
declarations: [DuplicateActionComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DuplicateActionComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should call duplicate action in drawing service', () => {
let drawing = { drawing_id: '1' } as Drawing;
component.drawings = [drawing];
component.nodes = [];
spyOn(mockedDrawingService, 'duplicate').and.returnValue(of());
component.duplicate();
expect(mockedDrawingService.duplicate).toHaveBeenCalled();
});
it('should call duplicate action in node service', () => {
let node = { node_id: '1', status: 'stopped'} as Node;
component.nodes = [node];
component.drawings = [];
spyOn(mockedNodeService, 'duplicate').and.returnValue(of());
component.duplicate();
expect(mockedNodeService.duplicate).toHaveBeenCalled();
});
it('should call duplicate action in both services', () => {
let drawing = { drawing_id: '1' } as Drawing;
component.drawings = [drawing];
let node = { node_id: '1', status: 'stopped' } as Node;
component.nodes = [node];
spyOn(mockedDrawingService, 'duplicate').and.returnValue(of());
spyOn(mockedNodeService, 'duplicate').and.returnValue(of());
component.duplicate();
expect(mockedDrawingService.duplicate).toHaveBeenCalled();
expect(mockedNodeService.duplicate).toHaveBeenCalled();
});
});

View File

@ -0,0 +1,51 @@
import { Component, OnInit, Input } from '@angular/core';
import { Server } from '../../../../../models/server';
import { Node } from '../../../../../cartography/models/node';
import { Drawing } from '../../../../../cartography/models/drawing';
import { Project } from '../../../../../models/project';
import { NodeService } from '../../../../../services/node.service';
import { DrawingService } from '../../../../../services/drawing.service';
import { NodesDataSource } from '../../../../../cartography/datasources/nodes-datasource';
import { DrawingsDataSource } from '../../../../../cartography/datasources/drawings-datasource';
import { ToasterService } from '../../../../../services/toaster.service';
@Component({
selector: 'app-duplicate-action',
templateUrl: './duplicate-action.component.html'
})
export class DuplicateActionComponent {
@Input() server: Server;
@Input() project: Project;
@Input() drawings: Drawing[];
@Input() nodes: Node[];
constructor(
private nodeService: NodeService,
private nodesDataSource: NodesDataSource,
private drawingService: DrawingService,
private drawingsDataSource: DrawingsDataSource,
private toasterService: ToasterService
) {}
duplicate() {
let runningNodes: string = '';
for(let node of this.nodes) {
if (node.status === 'stopped') {
this.nodeService.duplicate(this.server, node).subscribe((node: Node) => {
this.nodesDataSource.add(node);
});
} else {
runningNodes += `${node.name}, `;
}
}
for(let drawing of this.drawings) {
this.drawingService.duplicate(this.server, drawing.project_id, drawing).subscribe((drawing: Drawing) => {
this.drawingsDataSource.add(drawing);
})
}
runningNodes = runningNodes.substring(0, runningNodes.length-2);
this.toasterService.error(`Cannot duplicate node data for nodes: ${runningNodes}`);
}
}

View File

@ -4,6 +4,10 @@ import { Project } from '../../../../../models/project';
import { Drawing } from '../../../../../cartography/models/drawing';
import { MatDialog } from '@angular/material';
import { TextEditorDialogComponent } from '../../../drawings-editors/text-editor/text-editor.component';
import { Label } from '../../../../../cartography/models/label';
import { Node } from '../../../../../cartography/models/node';
import { Link } from '../../../../../models/link';
import { LinkNode } from '../../../../../models/link-node';
@Component({
selector: 'app-edit-text-action',
@ -13,6 +17,10 @@ export class EditTextActionComponent implements OnInit {
@Input() server: Server;
@Input() project: Project;
@Input() drawing: Drawing;
@Input() node: Node;
@Input() label: Label;
@Input() link: Link;
@Input() linkNode: LinkNode;
constructor(private dialog: MatDialog) {}
@ -27,5 +35,9 @@ export class EditTextActionComponent implements OnInit {
instance.server = this.server;
instance.project = this.project;
instance.drawing = this.drawing;
instance.node = this.node;
instance.label = this.label;
instance.link = this.link;
instance.linkNode = this.linkNode;
}
}

View File

@ -1,32 +1,45 @@
<div class="context-menu" [style.left]="leftPosition" [style.top]="topPosition">
<span [matMenuTriggerFor]="contextMenu"></span>
<mat-menu #contextMenu="matMenu" class="context-menu-items">
<app-start-node-action *ngIf="nodes.length" [server]="server" [nodes]="nodes"></app-start-node-action>
<app-stop-node-action *ngIf="nodes.length" [server]="server" [nodes]="nodes"></app-stop-node-action>
<app-start-node-action *ngIf="nodes.length && labels.length===0" [server]="server" [nodes]="nodes"></app-start-node-action>
<app-stop-node-action *ngIf="nodes.length && labels.length===0" [server]="server" [nodes]="nodes"></app-stop-node-action>
<app-console-device-action
*ngIf="!projectService.isReadOnly(project) && nodes.length && isElectronApp"
[server]="server"
[nodes]="nodes"
></app-console-device-action>
<app-edit-style-action *ngIf="drawings.length===1 && !hasTextCapabilities"
<app-duplicate-action *ngIf="drawings.length>0 || nodes.length>0"
[server]="server"
[project]="project"
[nodes]="nodes"
[drawings]="drawings"
></app-duplicate-action>
<app-edit-style-action *ngIf="!projectService.isReadOnly(project) && drawings.length===1 && !hasTextCapabilities"
[server]="server"
[project]="project"
[drawing]="drawings[0]"
></app-edit-style-action>
<app-edit-text-action
*ngIf="drawings.length===1 && hasTextCapabilities"
*ngIf="!projectService.isReadOnly(project) &&
(drawings.length===1 && hasTextCapabilities && labels.length===0 && linkNodes.length===0 ||
labels.length===1 && linkNodes.length===0 && drawings.length===0 ||
linkNodes.length===1 && labels.length===0 && drawings.length===0)"
[server]="server"
[project]="project"
[drawing]="drawings[0]"
[node]="nodes[0]"
[label]="labels[0]"
[link]="links[0]"
[linkNode]="linkNodes[0]"
></app-edit-text-action>
<app-move-layer-up-action
*ngIf="!projectService.isReadOnly(project) && (drawings.length || nodes.length)"
*ngIf="!projectService.isReadOnly(project) && (drawings.length || nodes.length) && labels.length===0"
[server]="server"
[nodes]="nodes"
[drawings]="drawings"
></app-move-layer-up-action>
<app-move-layer-down-action
*ngIf="!projectService.isReadOnly(project) && (drawings.length || nodes.length)"
*ngIf="!projectService.isReadOnly(project) && (drawings.length || nodes.length) && labels.length===0"
[server]="server"
[nodes]="nodes"
[drawings]="drawings"
@ -39,28 +52,28 @@
></app-start-capture-action>
<app-stop-capture-action
*ngIf="!projectService.isReadOnly(project) && isBundledServer
&& drawings.length===0 && nodes.length===0 && links.length===1"
&& drawings.length===0 && nodes.length===0 && links.length===1 && linkNodes.length === 0"
[server]="server"
[link]="links[0]"
></app-stop-capture-action>
<app-packet-filters-action
*ngIf="!projectService.isReadOnly(project) && drawings.length===0 && nodes.length===0 && links.length===1"
*ngIf="!projectService.isReadOnly(project) && drawings.length===0 && nodes.length===0 && links.length===1 && linkNodes.length === 0"
[server]="server"
[project]="project"
[link]="links[0]"
></app-packet-filters-action>
<app-resume-link-action
*ngIf="!projectService.isReadOnly(project) && drawings.length===0 && nodes.length===0 && links.length===1"
*ngIf="!projectService.isReadOnly(project) && drawings.length===0 && nodes.length===0 && links.length===1 && linkNodes.length === 0"
[server]="server"
[link]="links[0]"
></app-resume-link-action>
<app-suspend-link-action
*ngIf="!projectService.isReadOnly(project) && drawings.length===0 && nodes.length===0 && links.length===1"
*ngIf="!projectService.isReadOnly(project) && drawings.length===0 && nodes.length===0 && links.length===1 && linkNodes.length === 0"
[server]="server"
[link]="links[0]"
></app-suspend-link-action>
<app-delete-action
*ngIf="!projectService.isReadOnly(project)"
*ngIf="!projectService.isReadOnly(project) && (drawings.length>0 || nodes.length>0 || links.length>0) && linkNodes.length === 0"
[server]="server"
[nodes]="nodes"
[drawings]="drawings"

View File

@ -47,15 +47,6 @@ describe('ContextMenuComponent', () => {
expect(component.isElectronApp).toBeTruthy();
});
it('should reset capabilities while opening menu for node', () => {
component.contextMenu = { openMenu() {} } as MatMenuTrigger;
var spy = spyOn<any>(component, 'resetCapabilities');
component.openMenuForNode(null, 0, 0);
expect(spy.calls.any()).toBeTruthy();
});
it('should reset capabilities while opening menu for drawing', () => {
component.contextMenu = { openMenu() {} } as MatMenuTrigger;
let drawing = {} as Drawing;

View File

@ -10,6 +10,7 @@ import { TextElement } from '../../../cartography/models/drawings/text-element';
import { Label } from '../../../cartography/models/label';
import { Link } from '../../../models/link';
import { ElectronService } from 'ngx-electron';
import { LinkNode } from '../../../models/link-node';
@Component({
@ -21,7 +22,7 @@ export class ContextMenuComponent implements OnInit {
@Input() project: Project;
@Input() server: Server;
@ViewChild(MatMenuTrigger) contextMenu: MatMenuTrigger;
@ViewChild(MatMenuTrigger, {static: false}) contextMenu: MatMenuTrigger;
topPosition;
leftPosition;
@ -30,6 +31,7 @@ export class ContextMenuComponent implements OnInit {
nodes: Node[] = [];
labels: Label[] = [];
links: Link[] = [];
linkNodes: LinkNode[] = [];
hasTextCapabilities = false;
isElectronApp = false;
@ -74,10 +76,21 @@ export class ContextMenuComponent implements OnInit {
this.contextMenu.openMenu();
}
public openMenuForLabel(label: Label, top: number, left: number) {
public openMenuForLabel(label: Label, node: Node, top: number, left: number) {
this.resetCapabilities();
this.labels = [label];
this.nodes = [node];
this.setPosition(top, left);
this.contextMenu.openMenu();
}
public openMenuForInterfaceLabel(linkNode: LinkNode, link: Link, top: number, left: number) {
this.resetCapabilities();
this.linkNodes = [linkNode];
this.links = [link];
this.setPosition(top, left);
this.contextMenu.openMenu();
@ -99,6 +112,8 @@ export class ContextMenuComponent implements OnInit {
this.drawings = [];
this.nodes = [];
this.labels = [];
this.linkNodes = [];
this.links = [];
this.hasTextCapabilities = false;
}
}

View File

@ -19,7 +19,7 @@ import { NodeToMapNodeConverter } from '../../../cartography/converters/map/node
})
export class DrawLinkToolComponent implements OnInit, OnDestroy {
@Input() links: Link[];
@ViewChild(NodeSelectInterfaceComponent) nodeSelectInterfaceMenu: NodeSelectInterfaceComponent;
@ViewChild(NodeSelectInterfaceComponent, {static: false}) nodeSelectInterfaceMenu: NodeSelectInterfaceComponent;
private nodeClicked$: Subscription;

View File

@ -1,25 +1,27 @@
<h1 mat-dialog-title>Style editor</h1>
<div class="modal-form-container">
<mat-form-field>
<input matInput placeholder="Fill color" type="color" [(ngModel)]="element.fill">
</mat-form-field>
<form [formGroup]="formGroup">
<mat-form-field class="form-field">
<input matInput [ngModelOptions]="{standalone: true}" placeholder="Fill color" type="color" [(ngModel)]="element.fill">
</mat-form-field>
<mat-form-field>
<input matInput placeholder="Fill color" type="color" [(ngModel)]="element.stroke">
</mat-form-field>
<mat-form-field class="form-field">
<input matInput [ngModelOptions]="{standalone: true}" placeholder="Border color" type="color" [(ngModel)]="element.stroke">
</mat-form-field>
<mat-form-field>
<input matInput placeholder="Border width" type="text" [(ngModel)]="element.stroke_width">
</mat-form-field>
<mat-form-field *ngIf="element.stroke_dasharray">
<input matInput placeholder="Border style" type="text" [(ngModel)]="element.stroke_dasharray">
</mat-form-field>
<mat-form-field class="form-field">
<input matInput formControlName="borderWidth" placeholder="Border width" type="number">
</mat-form-field>
<mat-form-field class="form-field" *ngIf="element.stroke_dasharray">
<input matInput [ngModelOptions]="{standalone: true}" placeholder="Border style" type="text" [(ngModel)]="element.stroke_dasharray">
</mat-form-field>
<mat-form-field>
<input matInput placeholder="Rotation" type="text" [(ngModel)]="rotation">
</mat-form-field>
<mat-form-field class="form-field">
<input matInput formControlName="rotation" placeholder="Rotation" type="number">
</mat-form-field>
</form>
</div>
<div mat-dialog-actions>

View File

@ -47,3 +47,7 @@ input[type="color"]::-webkit-color-swatch {
.modal-form-container > * {
width: 100%;
}
.form-field {
width: 100%;
}

View File

@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, Injectable } from '@angular/core';
import { Server } from '../../../../models/server';
import { Project } from '../../../../models/project';
import { Drawing } from '../../../../cartography/models/drawing';
@ -10,6 +10,10 @@ import { DrawingsDataSource } from '../../../../cartography/datasources/drawings
import { EllipseElement } from '../../../../cartography/models/drawings/ellipse-element';
import { LineElement } from '../../../../cartography/models/drawings/line-element';
import { RectElement } from '../../../../cartography/models/drawings/rect-element';
import { FormGroup, FormBuilder, FormControl, Validators } from '@angular/forms';
import { ToasterService } from '../../../../services/toaster.service';
import { NonNegativeValidator } from '../../../../validators/non-negative-validator';
import { RotationValidator } from '../../../../validators/rotation-validator';
@Component({
selector: 'app-style-editor',
@ -21,19 +25,27 @@ export class StyleEditorDialogComponent implements OnInit {
project: Project;
drawing: Drawing;
element: ElementData;
rotation: string;
formGroup: FormGroup;
constructor(
public dialogRef: MatDialogRef<StyleEditorDialogComponent>,
private drawingToMapDrawingConverter: DrawingToMapDrawingConverter,
private mapDrawingToSvgConverter: MapDrawingToSvgConverter,
private drawingService: DrawingService,
private drawingsDataSource: DrawingsDataSource
) {}
private drawingsDataSource: DrawingsDataSource,
private formBuilder: FormBuilder,
private toasterService: ToasterService,
private nonNegativeValidator: NonNegativeValidator,
private rotationValidator: RotationValidator
) {
this.formGroup = this.formBuilder.group({
borderWidth: new FormControl('', [Validators.required, nonNegativeValidator.get]),
rotation: new FormControl('', [Validators.required, rotationValidator.get])
});
}
ngOnInit() {
this.element = new ElementData();
this.rotation = this.drawing.rotation.toString();
if (this.drawing.element instanceof RectElement || this.drawing.element instanceof EllipseElement) {
this.element.fill = this.drawing.element.fill;
@ -45,6 +57,10 @@ export class StyleEditorDialogComponent implements OnInit {
this.element.stroke_dasharray = this.drawing.element.stroke_dasharray;
this.element.stroke_width = this.drawing.element.stroke_width;
}
if (this.element.stroke_width === undefined) this.element.stroke_width = 0;
this.formGroup.controls['borderWidth'].setValue(this.element.stroke_width);
this.formGroup.controls['rotation'].setValue(this.drawing.rotation);
}
onNoClick() {
@ -52,27 +68,34 @@ export class StyleEditorDialogComponent implements OnInit {
}
onYesClick() {
this.drawing.rotation = +this.rotation;
if (this.drawing.element instanceof RectElement || this.drawing.element instanceof EllipseElement) {
this.drawing.element.fill = this.element.fill;
this.drawing.element.stroke = this.element.stroke;
this.drawing.element.stroke_dasharray = this.element.stroke_dasharray;
this.drawing.element.stroke_width = this.element.stroke_width;
} else if (this.drawing.element instanceof LineElement) {
this.drawing.element.stroke = this.element.stroke;
this.drawing.element.stroke_dasharray = this.element.stroke_dasharray;
this.drawing.element.stroke_width = this.element.stroke_width;
if (this.formGroup.valid) {
this.element.stroke_width = this.formGroup.get('borderWidth').value;
this.drawing.rotation = this.formGroup.get('rotation').value;
if (this.drawing.element instanceof RectElement || this.drawing.element instanceof EllipseElement) {
this.drawing.element.fill = this.element.fill;
this.drawing.element.stroke = this.element.stroke;
this.drawing.element.stroke_dasharray = this.element.stroke_dasharray;
this.drawing.element.stroke_width = this.element.stroke_width;
} else if (this.drawing.element instanceof LineElement) {
this.drawing.element.stroke = this.element.stroke;
this.drawing.element.stroke_dasharray = this.element.stroke_dasharray;
this.drawing.element.stroke_width = this.element.stroke_width;
}
let mapDrawing = this.drawingToMapDrawingConverter.convert(this.drawing);
mapDrawing.element = this.drawing.element;
this.drawing.svg = this.mapDrawingToSvgConverter.convert(mapDrawing);
this.drawingService.update(this.server, this.drawing).subscribe((serverDrawing: Drawing) => {
this.drawingsDataSource.update(serverDrawing);
this.dialogRef.close();
});
} else {
this.toasterService.error(`Entered data is incorrect`);
}
let mapDrawing = this.drawingToMapDrawingConverter.convert(this.drawing);
mapDrawing.element = this.drawing.element;
this.drawing.svg = this.mapDrawingToSvgConverter.convert(mapDrawing);
this.drawingService.update(this.server, this.drawing).subscribe((serverDrawing: Drawing) => {
this.drawingsDataSource.update(serverDrawing);
this.dialogRef.close();
});
}
}

View File

@ -1,15 +1,17 @@
<h1 mat-dialog-title>Text editor</h1>
<div class="modal-form-container">
<mat-form-field>
<mat-form-field class="form-field">
<input matInput placeholder="Fill color" type="color" (ngModelChange)="changeTextColor($event)" [(ngModel)]="element.fill">
</mat-form-field>
<mat-form-field>
<input matInput placeholder="Rotation" type="text" [(ngModel)]="rotation">
</mat-form-field>
<form [formGroup]="formGroup">
<mat-form-field class="form-field">
<input formControlName="rotation" matInput placeholder="Rotation" type="text">
</mat-form-field>
</form>
<textarea #textArea id="textArea" class="text" [(ngModel)]="element.text"> </textarea>
<textarea #textArea id="textArea" class="text" [(ngModel)]="element.text" [readonly]="!isTextEditable"></textarea>
</div>
<div mat-dialog-actions>

View File

@ -52,3 +52,7 @@ input[type="color"]::-webkit-color-swatch {
.modal-form-container > * {
width: 100%;
}
.form-field {
width: 100%;
}

View File

@ -8,6 +8,19 @@ import { MapDrawingToSvgConverter } from '../../../../cartography/converters/map
import { DrawingService } from '../../../../services/drawing.service';
import { DrawingsDataSource } from '../../../../cartography/datasources/drawings-datasource';
import { TextElement } from '../../../../cartography/models/drawings/text-element';
import { Label } from '../../../../cartography/models/label';
import { NodeService } from '../../../../services/node.service';
import { Node } from '../../../../cartography/models/node';
import { NodesDataSource } from '../../../../cartography/datasources/nodes-datasource';
import { Link } from '../../../../models/link';
import { LinkNode } from '../../../../models/link-node';
import { LinkService } from '../../../../services/link.service';
import { LinksDataSource } from '../../../../cartography/datasources/links-datasource';
import { FormBuilder, FormGroup, FormControl, Validators } from '@angular/forms';
import { ToasterService } from '../../../../services/toaster.service';
import { RotationValidator } from '../../../../validators/rotation-validator';
import { Font } from '../../../../cartography/models/font';
import { FontFixer } from '../../../../cartography/helpers/font-fixer';
@Component({
selector: 'app-text-editor',
@ -15,13 +28,19 @@ import { TextElement } from '../../../../cartography/models/drawings/text-elemen
styleUrls: ['./text-editor.component.scss']
})
export class TextEditorDialogComponent implements OnInit {
@ViewChild('textArea') textArea: ElementRef;
@ViewChild('textArea', {static: true}) textArea: ElementRef;
server: Server;
project: Project;
drawing: Drawing;
node: Node;
label: Label;
link: Link;
linkNode: LinkNode;
element: TextElement;
rotation: string;
isTextEditable: boolean;
formGroup: FormGroup;
constructor(
private dialogRef: MatDialogRef<TextEditorDialogComponent>,
@ -29,17 +48,74 @@ export class TextEditorDialogComponent implements OnInit {
private mapDrawingToSvgConverter: MapDrawingToSvgConverter,
private drawingService: DrawingService,
private drawingsDataSource: DrawingsDataSource,
private renderer: Renderer2
private renderer: Renderer2,
private nodeService: NodeService,
private nodesDataSource: NodesDataSource,
private linkService: LinkService,
private linksDataSource: LinksDataSource,
private formBuilder: FormBuilder,
private toasterService: ToasterService,
private rotationValidator: RotationValidator,
private fontFixer: FontFixer
) {}
ngOnInit() {
this.rotation = this.drawing.rotation.toString();
this.formGroup = this.formBuilder.group({
rotation: new FormControl('', [Validators.required, this.rotationValidator.get])
});
this.element = this.drawing.element as TextElement;
if (this.label && this.node) {
this.isTextEditable = false;
this.rotation = this.label.rotation.toString();
this.element = this.getTextElementFromLabel();
} else if (this.linkNode && this.link) {
this.isTextEditable = true;
this.label = this.link.nodes.find(n => n.node_id === this.linkNode.node_id).label;
this.rotation = this.label.rotation.toString();
this.element = this.getTextElementFromLabel();
} else if (this.drawing) {
this.isTextEditable = true;
this.rotation = this.drawing.rotation.toString();
this.element = this.drawing.element as TextElement;
};
let font: Font = {
font_family: this.element.font_family,
font_size: this.element.font_size,
font_weight: this.element.font_weight
};
font = this.fontFixer.fix(font);
this.formGroup.controls['rotation'].setValue(this.rotation);
this.renderer.setStyle(this.textArea.nativeElement, 'color', this.element.fill);
this.renderer.setStyle(this.textArea.nativeElement, 'font-family', this.element.font_family);
this.renderer.setStyle(this.textArea.nativeElement, 'font-size', `${this.element.font_size}pt`);
this.renderer.setStyle(this.textArea.nativeElement, 'font-weight', this.element.font_weight);
this.renderer.setStyle(this.textArea.nativeElement, 'font-family', font.font_family);
this.renderer.setStyle(this.textArea.nativeElement, 'font-size', `${font.font_size}pt`);
this.renderer.setStyle(this.textArea.nativeElement, 'font-weight', font.font_weight);
}
getTextElementFromLabel(): TextElement{
var styleProperties: StyleProperty[] = [];
var textElement = new TextElement();
for (var property of this.label.style.split(";")){
styleProperties.push({
property: property.split(": ")[0],
value: property.split(": ")[1]
});
}
textElement.text = this.label.text ? this.label.text : '';
textElement.font_family = styleProperties.find(p => p.property === 'font-family') ? styleProperties.find(p => p.property === 'font-family').value : 'TypeWriter';
textElement.font_size = styleProperties.find(p => p.property === 'font-size') ? +styleProperties.find(p => p.property === 'font-size').value : 10.0;
textElement.font_weight = styleProperties.find(p => p.property === 'font-weight') ? styleProperties.find(p => p.property === 'font-weight').value : 'normal';
textElement.fill = styleProperties.find(p => p.property === 'fill') ? styleProperties.find(p => p.property === 'fill').value : '#000000';
textElement.fill_opacity = styleProperties.find(p => p.property === 'fill-opacity') ? +styleProperties.find(p => p.property === 'fill-opacity').value : 1.0;
return textElement;
}
getStyleFromTextElement(): string{
return `font-family: ${this.element.font_family};font-size: ${this.element.font_size};font-weight: ${this.element.font_weight};fill: ${this.element.fill};fill-opacity: ${this.element.fill_opacity};`;
}
onNoClick() {
@ -47,21 +123,51 @@ export class TextEditorDialogComponent implements OnInit {
}
onYesClick() {
this.drawing.rotation = +this.rotation;
this.drawing.element = this.element;
if (this.formGroup.valid) {
this.rotation = this.formGroup.get('rotation').value;
let mapDrawing = this.drawingToMapDrawingConverter.convert(this.drawing);
mapDrawing.element = this.drawing.element;
this.drawing.svg = this.mapDrawingToSvgConverter.convert(mapDrawing);
this.drawingService.update(this.server, this.drawing).subscribe((serverDrawing: Drawing) => {
this.drawingsDataSource.update(serverDrawing);
this.dialogRef.close();
});
if (this.label && this.node) {
this.node.label.style = this.getStyleFromTextElement();
this.node.label.rotation = +this.rotation;
this.nodeService.updateLabel(this.server, this.node, this.node.label).subscribe((node: Node) => {
this.nodesDataSource.update(node);
this.dialogRef.close();
});
} else if (this.linkNode && this.link) {
this.label.style = this.getStyleFromTextElement();
this.label.rotation = +this.rotation;
this.label.text = this.element.text;
this.linkService.updateLink(this.server, this.link).subscribe((link: Link) => {
this.linksDataSource.update(link);
this.dialogRef.close();
});
} else if (this.drawing) {
this.drawing.rotation = +this.rotation;
this.drawing.element = this.element;
let mapDrawing = this.drawingToMapDrawingConverter.convert(this.drawing);
mapDrawing.element = this.drawing.element;
this.drawing.svg = this.mapDrawingToSvgConverter.convert(mapDrawing);
this.drawingService.update(this.server, this.drawing).subscribe((serverDrawing: Drawing) => {
this.drawingsDataSource.update(serverDrawing);
this.dialogRef.close();
});
};
} else {
this.toasterService.error(`Entered data is incorrect`);
}
}
changeTextColor(changedColor) {
this.renderer.setStyle(this.textArea.nativeElement, 'color', changedColor);
}
}
export interface StyleProperty {
property: string;
value: string;
}

View File

@ -15,7 +15,7 @@ export class NodeSelectInterfaceComponent implements OnInit {
@Input() links: Link[];
@Output() onChooseInterface = new EventEmitter<any>();
@ViewChild(MatMenuTrigger) contextMenu: MatMenuTrigger;
@ViewChild(MatMenuTrigger, {static: false}) contextMenu: MatMenuTrigger;
protected topPosition;
protected leftPosition;

View File

@ -0,0 +1,65 @@
<button
matTooltip="Add a note"
mat-icon-button
class="menu-button"
[color]="drawTools.isTextChosen ? 'primary' : 'basic'"
(click)="addDrawing('text')">
<mat-icon>create</mat-icon>
</button>
<input
type="file"
accept=".svg, .bmp, .jpeg, .jpg, .gif, .png"
class="non-visible"
#file
(change)="uploadImageFile($event)"/>
<button
matTooltip="Insert a picture"
mat-icon-button
class="menu-button"
(click)="file.click()">
<mat-icon>image</mat-icon>
</button>
<button
matTooltip="Draw a rectangle"
mat-icon-button
class="menu-button"
[color]="drawTools.isRectangleChosen ? 'primary' : 'basic'"
(click)="addDrawing('rectangle')">
<mat-icon>crop_3_2</mat-icon>
</button>
<button
matTooltip="Draw an ellipse"
mat-icon-button
class="menu-button"
[color]="drawTools.isEllipseChosen ? 'primary' : 'basic'"
(click)="addDrawing('ellipse')">
<mat-icon>panorama_fish_eye</mat-icon>
</button>
<button
matTooltip="Draw a line"
mat-icon-button class="menu-button"
(click)="addDrawing('line')">
<svg height="40" width="40">
<line
[ngClass]="{ selected: drawTools.isLineChosen }"
x1="30"
y1="10"
x2="10"
y2="30"
style="stroke:white;stroke-width:2"/>
</svg>
</button>
<button
matTooltip="Lock or unlock all items"
mat-icon-button
class="menu-button"
[color]="isLocked ? 'primary' : 'basic'"
(click)="changeLockValue()">
<mat-icon>lock</mat-icon>
</button>
<app-drawing-added
[server]="server"
[project]="project"
[selectedDrawing]="selectedDrawing"
(drawingSaved)="onDrawingSaved()">
</app-drawing-added>

View File

@ -0,0 +1,24 @@
.menu-button {
outline: 0 !important;
transition: 0.5s;
margin-bottom: 16px;
width: 40px;
margin-right: 12px !important;
margin-left: 12px !important;
background: #263238;
padding: 0;
border: none;
background-color: transparent;
}
mat-divider.divider {
height: 40px;
margin-left: 1px;
margin-right: 7px;
width: 10px;
color: gray;
}
.non-visible {
display: none;
}

View File

@ -0,0 +1,67 @@
import { ProjectMapMenuComponent } from "./project-map-menu.component";
import { ComponentFixture, async, TestBed } from '@angular/core/testing';
import { MockedDrawingService } from '../project-map.component.spec';
import { MapSettingService } from '../../../services/mapsettings.service';
import { MatIconModule, MatToolbarModule, MatMenuModule, MatCheckboxModule } from '@angular/material';
import { CommonModule } from '@angular/common';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { DrawingService } from '../../../services/drawing.service';
import { ToolsService } from '../../../services/tools.service';
import { D3MapComponent } from '../../../cartography/components/d3-map/d3-map.component';
import { ANGULAR_MAP_DECLARATIONS } from '../../../cartography/angular-map.imports';
import { NO_ERRORS_SCHEMA } from '@angular/core';
describe('ProjectMapMenuComponent', () => {
let component: ProjectMapMenuComponent;
let fixture: ComponentFixture<ProjectMapMenuComponent>;
let drawingService = new MockedDrawingService();
let mapSettingService = new MapSettingService();
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [MatIconModule, MatToolbarModule, MatMenuModule, MatCheckboxModule, CommonModule, NoopAnimationsModule],
providers: [
{ provide: DrawingService, useValue: drawingService },
{ provide: ToolsService },
{ provide: MapSettingService, useValue: mapSettingService }
],
declarations: [ProjectMapMenuComponent, D3MapComponent, ...ANGULAR_MAP_DECLARATIONS],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ProjectMapMenuComponent);
component = fixture.componentInstance;
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should reset choice on draw menu after saving drawing', () => {
spyOn(component, 'resetDrawToolChoice');
component.onDrawingSaved();
expect(component.resetDrawToolChoice).toHaveBeenCalled();
});
it('should call map settings service when lock value was changed', () => {
spyOn(mapSettingService, 'changeMapLockValue');
component.changeLockValue();
expect(mapSettingService.changeMapLockValue).toHaveBeenCalled();
});
it('should call map settings service with proper value', () => {
spyOn(mapSettingService, 'changeMapLockValue');
component.changeLockValue();
expect(mapSettingService.changeMapLockValue).toHaveBeenCalledWith(true);
component.changeLockValue();
expect(mapSettingService.changeMapLockValue).toHaveBeenCalledWith(false);
});
});

View File

@ -0,0 +1,110 @@
import { Component, OnInit, OnDestroy, Input } from '@angular/core';
import { Project } from '../../../models/project';
import { Server } from '../../../models/server';
import { ToolsService } from '../../../services/tools.service';
import { MapSettingService } from '../../../services/mapsettings.service';
import { DrawingService } from '../../../services/drawing.service';
@Component({
selector: 'app-project-map-menu',
templateUrl: './project-map-menu.component.html',
styleUrls: ['./project-map-menu.component.scss']
})
export class ProjectMapMenuComponent implements OnInit, OnDestroy {
@Input() project: Project;
@Input() server: Server;
public selectedDrawing: string;
public drawTools = {
isRectangleChosen: false,
isEllipseChosen: false,
isLineChosen: false,
isTextChosen: false
};
public isLocked: boolean = false;
constructor(
private toolsService: ToolsService,
private mapSettingsService: MapSettingService,
private drawingService: DrawingService
) {}
ngOnInit() {}
public addDrawing(selectedObject: string) {
switch (selectedObject) {
case 'rectangle':
this.drawTools.isTextChosen = false;
this.drawTools.isEllipseChosen = false;
this.drawTools.isRectangleChosen = !this.drawTools.isRectangleChosen;
this.drawTools.isLineChosen = false;
break;
case 'ellipse':
this.drawTools.isTextChosen = false;
this.drawTools.isEllipseChosen = !this.drawTools.isEllipseChosen;
this.drawTools.isRectangleChosen = false;
this.drawTools.isLineChosen = false;
break;
case 'line':
this.drawTools.isTextChosen = false;
this.drawTools.isEllipseChosen = false;
this.drawTools.isRectangleChosen = false;
this.drawTools.isLineChosen = !this.drawTools.isLineChosen;
break;
case 'text':
this.drawTools.isTextChosen = !this.drawTools.isTextChosen;
this.drawTools.isEllipseChosen = false;
this.drawTools.isRectangleChosen = false;
this.drawTools.isLineChosen = false;
this.toolsService.textAddingToolActivation(this.drawTools.isTextChosen);
break;
}
this.selectedDrawing = this.selectedDrawing === selectedObject ? '' : selectedObject;
}
public onDrawingSaved() {
this.resetDrawToolChoice();
}
public resetDrawToolChoice() {
this.drawTools.isRectangleChosen = false;
this.drawTools.isEllipseChosen = false;
this.drawTools.isLineChosen = false;
this.drawTools.isTextChosen = false;
this.selectedDrawing = '';
this.toolsService.textAddingToolActivation(this.drawTools.isTextChosen);
}
public changeLockValue() {
this.isLocked = !this.isLocked;
this.mapSettingsService.changeMapLockValue(this.isLocked);
}
public uploadImageFile(event) {
this.readImageFile(event.target);
}
private readImageFile(fileInput) {
let file: File = fileInput.files[0];
let fileReader: FileReader = new FileReader();
let imageToUpload = new Image();
fileReader.onloadend = () => {
let image = fileReader.result;
let svg = this.createSvgFileForImage(image, imageToUpload);
this.drawingService.add(this.server, this.project.project_id, -(imageToUpload.width/2), -(imageToUpload.height/2), svg).subscribe(() => {});
}
imageToUpload.onload = () => { fileReader.readAsDataURL(file) };
imageToUpload.src = window.URL.createObjectURL(file);
}
private createSvgFileForImage(image: string|ArrayBuffer, imageToUpload: HTMLImageElement) {
return `<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" height=\"${imageToUpload.height}\"
width=\"${imageToUpload.width}\">\n<image height=\"${imageToUpload.height}\" width=\"${imageToUpload.width}\" xlink:href=\"${image}\"/>\n</svg>`
}
ngOnDestroy() {}
}

View File

@ -1,6 +1,7 @@
<div *ngIf="project" class="project-map">
<app-d3-map
*ngIf="!settings.angular_map"
[server]="server"
[symbols]="symbols"
[nodes]="nodes"
[links]="links"
@ -87,86 +88,30 @@
</mat-toolbar>
</div>
<div id="show-menu-wrapper" [ngClass]="{ shadowed: !drawTools.visibility }" *ngIf="!readonly">
<div id="show-menu-wrapper" [ngClass]="{ shadowed: !isProjectMapMenuVisible }" *ngIf="!readonly">
<button class="arrow-button" mat-icon-button (click)="showMenu()"><mat-icon>keyboard_arrow_right</mat-icon></button>
</div>
<div id="menu-wrapper" [ngClass]="{ extended: drawTools.visibility }">
<div id="menu-wrapper" [ngClass]="{ extended: isProjectMapMenuVisible }">
<app-nodes-menu [server]="server" [project]="project"></app-nodes-menu>
<mat-divider class="divider" [vertical]="true"></mat-divider>
<button
matTooltip="Add a note"
mat-icon-button
class="menu-button"
[color]="drawTools.isTextChosen ? 'primary' : 'basic'"
(click)="addDrawing('text')"
>
<mat-icon>create</mat-icon>
</button>
<input
type="file"
accept=".svg, .bmp, .jpeg, .jpg, .gif, .png"
class="non-visible"
#file
(change)="uploadImageFile($event)"
/>
<button
matTooltip="Insert a picture"
mat-icon-button
class="menu-button"
(click)="file.click()"
>
<mat-icon>image</mat-icon>
</button>
<button
matTooltip="Draw a rectangle"
mat-icon-button
class="menu-button"
[color]="drawTools.isRectangleChosen ? 'primary' : 'basic'"
(click)="addDrawing('rectangle')"
>
<mat-icon>crop_3_2</mat-icon>
</button>
<button
matTooltip="Draw an ellipse"
mat-icon-button
class="menu-button"
[color]="drawTools.isEllipseChosen ? 'primary' : 'basic'"
(click)="addDrawing('ellipse')"
>
<mat-icon>panorama_fish_eye</mat-icon>
</button>
<button matTooltip="Draw a line" mat-icon-button class="menu-button" (click)="addDrawing('line')">
<svg height="40" width="40">
<line
[ngClass]="{ selected: drawTools.isLineChosen }"
x1="30"
y1="10"
x2="10"
y2="30"
style="stroke:white;stroke-width:2"
/>
</svg>
</button>
<app-project-map-menu [server]="server" [project]="project"></app-project-map-menu>
<button class="arrow-button" mat-icon-button (click)="hideMenu()"><mat-icon>keyboard_arrow_left</mat-icon></button>
</div>
<app-context-menu [project]="project" [server]="server"></app-context-menu>
</div>
<div id="zoom-buttons">
<button class="zoom-button" (click)="zoomIn()">+</button>
<button class="zoom-button" (click)="resetZoom()"><mat-icon>adjust</mat-icon></button>
<button class="zoom-button" (click)="zoomOut()">-</button>
</div>
<app-progress></app-progress>
<app-project-map-shortcuts *ngIf="project" [project]="project" [server]="server"> </app-project-map-shortcuts>
<app-project-map-shortcuts *ngIf="project" [project]="project" [server]="server"></app-project-map-shortcuts>
<app-draw-link-tool [links]="links" *ngIf="tools.draw_link"></app-draw-link-tool>
<app-drawing-added
[server]="server"
[project]="project"
[selectedDrawing]="selectedDrawing"
(drawingSaved)="onDrawingSaved()"
>
</app-drawing-added>
<app-drawing-dragged [server]="server"></app-drawing-dragged>
<app-drawing-resized [server]="server"></app-drawing-resized>
<app-interface-label-dragged [server]="server"></app-interface-label-dragged>

View File

@ -80,7 +80,7 @@ g.node:hover {
}
.extended {
width: 640px !important;
width: 700px !important;
height: 100%;
overflow: hidden;
}
@ -93,6 +93,44 @@ mat-divider.divider {
color: gray;
}
#zoom-buttons {
position: fixed;
background: #263238;
bottom: 20px;
right: 20px;
display: grid;
.zoom-button {
outline: none;
height: 40px;
width: 40px;
background: #263238;
border: none;
color: white;
font-size: 1.25rem;
font-weight: bold;
mat-icon {
margin-top: 8px;
}
}
.zoom-button-white {
outline: none;
height: 40px;
width: 40px;
color: #263238;
border: none;
background: white;
font-size: 1.25rem;
font-weight: bold;
mat-icon {
margin-top: 8px;
}
}
}
@-moz-document url-prefix() {
/** fixes gray background of drawing menu on Firefox **/
.mat-drawer-content {

View File

@ -40,9 +40,18 @@ import { RecentlyOpenedProjectService } from '../../services/recentlyOpenedProje
import { MapLinkToLinkConverter } from '../../cartography/converters/map/map-link-to-link-converter';
import { Link } from '../../models/link';
import { Project } from '../../models/project';
import { MovingEventSource } from '../../cartography/events/moving-event-source';
import { CapturingSettings } from '../../models/capturingSettings';
import { LinkWidget } from '../../cartography/widgets/link';
import { MapScaleService } from '../../services/mapScale.service';
import { NodeCreatedLabelStylesFixer } from './helpers/node-created-label-styles-fixer';
import { LabelWidget } from '../../cartography/widgets/label';
import { InterfaceLabelWidget } from '../../cartography/widgets/interface-label';
import { MapLinkNodeToLinkNodeConverter } from '../../cartography/converters/map/map-link-node-to-link-node-converter';
import { MapSettingService } from '../../services/mapsettings.service';
import { ProjectMapMenuComponent } from './project-map-menu/project-map-menu.component';
import { MockedToasterService } from '../../services/toaster.service.spec';
import { ToasterService } from '../../services/toaster.service';
export class MockedProgressService {
public activate() {}
@ -81,6 +90,10 @@ export class MockedNodeService {
reloadAll(server: Server, project: Project) {
return of();
}
duplicate(server: Server, node: Node) {
return of(node);
}
}
export class MockedDrawingService {
@ -91,6 +104,10 @@ export class MockedDrawingService {
return of(this.drawing);
}
duplicate(server: Server, project_id: string, drawing: Drawing) {
return of(drawing);
}
updatePosition(_server: Server, _drawing: Drawing, _x: number, _y: number) {
return of(this.drawing);
}
@ -183,6 +200,7 @@ describe('ProjectMapComponent', () => {
let drawingsDataSource = new MockedDrawingsDataSource();
let nodesDataSource = new MockedNodesDataSource();
let linksDataSource = new MockedLinksDataSource();
let mockedToasterService = new MockedToasterService();
let nodeCreatedLabelStylesFixer;
beforeEach(async(() => {
@ -205,10 +223,13 @@ describe('ProjectMapComponent', () => {
{ provide: NodeWidget },
{ provide: LinkWidget },
{ provide: DrawingsWidget },
{ provide: LabelWidget },
{ provide: InterfaceLabelWidget },
{ provide: MapNodeToNodeConverter },
{ provide: MapDrawingToDrawingConverter },
{ provide: MapLabelToLabelConverter },
{ provide: MapLinkToLinkConverter },
{ provide: MapLinkNodeToLinkNodeConverter },
{ provide: NodesDataSource, useValue: nodesDataSource },
{ provide: LinksDataSource, useValue: linksDataSource },
{ provide: DrawingsDataSource, useValue: drawingsDataSource },
@ -216,13 +237,17 @@ describe('ProjectMapComponent', () => {
{ provide: ToolsService },
{ provide: SelectionManager },
{ provide: SelectionTool },
{ provide: MovingEventSource },
{
provide: RecentlyOpenedProjectService,
useClass: RecentlyOpenedProjectService
},
{ provide: NodeCreatedLabelStylesFixer, useValue: nodeCreatedLabelStylesFixer}
{ provide: NodeCreatedLabelStylesFixer, useValue: nodeCreatedLabelStylesFixer},
{ provide: MapScaleService },
{ provide: NodeCreatedLabelStylesFixer, useValue: nodeCreatedLabelStylesFixer},
{ provide: ToasterService, useValue: mockedToasterService }
],
declarations: [ProjectMapComponent, D3MapComponent, ...ANGULAR_MAP_DECLARATIONS],
declarations: [ProjectMapComponent, ProjectMapMenuComponent, D3MapComponent, ...ANGULAR_MAP_DECLARATIONS],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
@ -230,6 +255,13 @@ describe('ProjectMapComponent', () => {
beforeEach(() => {
fixture = TestBed.createComponent(ProjectMapComponent);
component = fixture.componentInstance;
component.projectMapMenuComponent = {
resetDrawToolChoice(){}
} as ProjectMapMenuComponent;
component.ws = {
OPEN: 0,
} as WebSocket;
});
afterEach(() => {
@ -245,18 +277,10 @@ describe('ProjectMapComponent', () => {
document.getElementsByClassName = jasmine.createSpy('HTML element').and.callFake(() => {
return [dummyElement];
});
spyOn(component, 'resetDrawToolChoice');
spyOn(component.projectMapMenuComponent, 'resetDrawToolChoice').and.returnValue();
component.hideMenu();
expect(component.resetDrawToolChoice).toHaveBeenCalled();
});
it('should reset choice on draw menu after saving drawing', () => {
spyOn(component, 'resetDrawToolChoice');
component.onDrawingSaved();
expect(component.resetDrawToolChoice).toHaveBeenCalled();
expect(component.projectMapMenuComponent.resetDrawToolChoice).toHaveBeenCalled();
});
});

View File

@ -30,7 +30,7 @@ import { MapNodeToNodeConverter } from '../../cartography/converters/map/map-nod
import { SettingsService, Settings } from '../../services/settings.service';
import { D3MapComponent } from '../../cartography/components/d3-map/d3-map.component';
import { ToolsService } from '../../services/tools.service';
import { DrawingContextMenu, LinkContextMenu } from '../../cartography/events/event-source';
import { DrawingContextMenu, LinkContextMenu, LabelContextMenu, InterfaceLabelContextMenu } from '../../cartography/events/event-source';
import { MapDrawingToDrawingConverter } from '../../cartography/converters/map/map-drawing-to-drawing-converter';
import { SelectionManager } from '../../cartography/managers/selection-manager';
import { SelectionTool } from '../../cartography/tools/selection-tool';
@ -42,8 +42,15 @@ import { MapLabelToLabelConverter } from '../../cartography/converters/map/map-l
import { RecentlyOpenedProjectService } from '../../services/recentlyOpenedProject.service';
import { MapLink } from '../../cartography/models/map/map-link';
import { MapLinkToLinkConverter } from '../../cartography/converters/map/map-link-to-link-converter';
import { MovingEventSource } from '../../cartography/events/moving-event-source';
import { LinkWidget } from '../../cartography/widgets/link';
import { MapScaleService } from '../../services/mapScale.service';
import { NodeCreatedLabelStylesFixer } from './helpers/node-created-label-styles-fixer';
import { InterfaceLabelWidget } from '../../cartography/widgets/interface-label';
import { LabelWidget } from '../../cartography/widgets/label';
import { MapLinkNodeToLinkNodeConverter } from '../../cartography/converters/map/map-link-node-to-link-node-converter';
import { ProjectMapMenuComponent } from './project-map-menu/project-map-menu.component';
import { ToasterService } from '../../services/toaster.service';
@Component({
@ -59,8 +66,8 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
public symbols: Symbol[] = [];
public project: Project;
public server: Server;
public selectedDrawing: string;
private ws: Subject<any>;
public ws: WebSocket;
public isProjectMapMenuVisible: boolean = false;
tools = {
selection: true,
@ -70,19 +77,11 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
};
protected settings: Settings;
protected drawTools = {
isRectangleChosen: false,
isEllipseChosen: false,
isLineChosen: false,
isTextChosen: false,
visibility: false
};
private inReadOnlyMode = false;
@ViewChild(ContextMenuComponent) contextMenu: ContextMenuComponent;
@ViewChild(D3MapComponent) mapChild: D3MapComponent;
@ViewChild(ContextMenuComponent, {static: false}) contextMenu: ContextMenuComponent;
@ViewChild(D3MapComponent, {static: false}) mapChild: D3MapComponent;
@ViewChild(ProjectMapMenuComponent, {static: false}) projectMapMenuComponent: ProjectMapMenuComponent;
private subscriptions: Subscription[] = [];
@ -98,10 +97,13 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
private nodeWidget: NodeWidget,
private drawingsWidget: DrawingsWidget,
private linkWidget: LinkWidget,
private labelWidget: LabelWidget,
private interfaceLabelWidget: InterfaceLabelWidget,
private mapNodeToNode: MapNodeToNodeConverter,
private mapDrawingToDrawing: MapDrawingToDrawingConverter,
private mapLabelToLabel: MapLabelToLabelConverter,
private mapLinkToLink: MapLinkToLinkConverter,
private mapLinkNodeToLinkNode: MapLinkNodeToLinkNodeConverter,
private nodesDataSource: NodesDataSource,
private linksDataSource: LinksDataSource,
private drawingsDataSource: DrawingsDataSource,
@ -110,7 +112,10 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
private selectionManager: SelectionManager,
private selectionTool: SelectionTool,
private recentlyOpenedProjectService: RecentlyOpenedProjectService,
private nodeCreatedLabelStylesFixer: NodeCreatedLabelStylesFixer
private movingEventSource: MovingEventSource,
private mapScaleService: MapScaleService,
private nodeCreatedLabelStylesFixer: NodeCreatedLabelStylesFixer,
private toasterService: ToasterService
) {}
ngOnInit() {
@ -184,6 +189,18 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
this.mapChangeDetectorRef.detectChanges();
})
);
this.addKeyboardListeners();
}
addKeyboardListeners() {
Mousetrap.bind('ctrl++', (event: Event) => {
event.preventDefault();
});
Mousetrap.bind('ctrl+-', (event: Event) => {
event.preventDefault();
});;
}
onProjectLoad(project: Project) {
@ -213,9 +230,15 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
}
setUpWS(project: Project) {
this.ws = webSocket(this.projectService.notificationsPath(this.server, project.project_id));
this.ws = new WebSocket(this.projectService.notificationsPath(this.server, project.project_id));
this.subscriptions.push(this.projectWebServiceHandler.connect(this.ws));
this.ws.onmessage = (event: MessageEvent) => {
this.projectWebServiceHandler.handleMessage(JSON.parse(event.data));
};
this.ws.onerror = (event: MessageEvent) => {
this.toasterService.error('Connection to host lost.');
};
}
setUpMapCallbacks() {
@ -238,9 +261,21 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
this.contextMenu.openMenuForDrawing(drawing, eventDrawing.event.pageY, eventDrawing.event.pageX);
});
const onLabelContextMenu = this.labelWidget.onContextMenu.subscribe((eventLabel: LabelContextMenu) => {
const label = this.mapLabelToLabel.convert(eventLabel.label);
const node = this.nodes.find(n => n.node_id === eventLabel.label.nodeId);
this.contextMenu.openMenuForLabel(label, node, eventLabel.event.pageY, eventLabel.event.pageX);
});
const onInterfaceLabelContextMenu = this.interfaceLabelWidget.onContextMenu.subscribe((eventInterfaceLabel: InterfaceLabelContextMenu) => {
const linkNode = this.mapLinkNodeToLinkNode.convert(eventInterfaceLabel.interfaceLabel);
const link = this.links.find(l => l.link_id === eventInterfaceLabel.interfaceLabel.linkId);
this.contextMenu.openMenuForInterfaceLabel(linkNode, link, eventInterfaceLabel.event.pageY, eventInterfaceLabel.event.pageX);
});
const onContextMenu = this.selectionTool.contextMenuOpened.subscribe((event) => {
const selectedItems = this.selectionManager.getSelected();
if (selectedItems.length === 0 || !(event instanceof MouseEvent)) return;
if (selectedItems.length < 2 || !(event instanceof MouseEvent)) return;
let drawings: Drawing[] = [];
let nodes: Node[] = [];
@ -266,6 +301,8 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
this.subscriptions.push(onNodeContextMenu);
this.subscriptions.push(onDrawingContextMenu);
this.subscriptions.push(onContextMenu);
this.subscriptions.push(onLabelContextMenu);
this.subscriptions.push(onInterfaceLabelContextMenu);
this.mapChangeDetectorRef.detectChanges();
}
@ -288,7 +325,7 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
}
public onDrawingSaved() {
this.resetDrawToolChoice();
this.projectMapMenuComponent.resetDrawToolChoice();
}
public set readonly(value) {
@ -308,7 +345,8 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
public toggleMovingMode() {
this.tools.moving = !this.tools.moving;
this.toolsService.movingToolActivation(this.tools.moving);
this.movingEventSource.movingModeState.emit(this.tools.moving);
if (!this.readonly) {
this.tools.selection = !this.tools.moving;
this.toolsService.selectionToolActivation(this.tools.selection);
@ -324,56 +362,31 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
this.project.show_interface_labels = enabled;
}
public addDrawing(selectedObject: string) {
switch (selectedObject) {
case 'rectangle':
this.drawTools.isTextChosen = false;
this.drawTools.isEllipseChosen = false;
this.drawTools.isRectangleChosen = !this.drawTools.isRectangleChosen;
this.drawTools.isLineChosen = false;
break;
case 'ellipse':
this.drawTools.isTextChosen = false;
this.drawTools.isEllipseChosen = !this.drawTools.isEllipseChosen;
this.drawTools.isRectangleChosen = false;
this.drawTools.isLineChosen = false;
break;
case 'line':
this.drawTools.isTextChosen = false;
this.drawTools.isEllipseChosen = false;
this.drawTools.isRectangleChosen = false;
this.drawTools.isLineChosen = !this.drawTools.isLineChosen;
break;
case 'text':
this.drawTools.isTextChosen = !this.drawTools.isTextChosen;
this.drawTools.isEllipseChosen = false;
this.drawTools.isRectangleChosen = false;
this.drawTools.isLineChosen = false;
this.toolsService.textAddingToolActivation(this.drawTools.isTextChosen);
break;
}
this.selectedDrawing = this.selectedDrawing === selectedObject ? '' : selectedObject;
}
public resetDrawToolChoice() {
this.drawTools.isRectangleChosen = false;
this.drawTools.isEllipseChosen = false;
this.drawTools.isLineChosen = false;
this.drawTools.isTextChosen = false;
this.selectedDrawing = '';
this.toolsService.textAddingToolActivation(this.drawTools.isTextChosen);
}
public hideMenu() {
this.resetDrawToolChoice();
this.drawTools.visibility = false;
this.projectMapMenuComponent.resetDrawToolChoice()
this.isProjectMapMenuVisible = false;
}
public showMenu() {
this.drawTools.visibility = true;
this.isProjectMapMenuVisible = true;
}
zoomIn() {
this.mapScaleService.setScale(this.mapScaleService.getScale() + 0.1);
}
zoomOut() {
let currentScale = this.mapScaleService.getScale();
if ((currentScale - 0.1) > 0) {
this.mapScaleService.setScale(currentScale - 0.1);
}
}
resetZoom() {
this.mapScaleService.resetToDefault();
}
public uploadImageFile(event) {
this.readImageFile(event.target);
}
@ -400,8 +413,8 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
this.nodesDataSource.clear();
this.linksDataSource.clear();
if (this.ws) {
this.ws.unsubscribe();
if (this.ws.OPEN) {
this.ws.close();
}
this.subscriptions.forEach((subscription: Subscription) => subscription.unsubscribe());
}

View File

@ -29,7 +29,7 @@ export class ProjectsComponent implements OnInit {
displayedColumns = ['name', 'actions'];
settings: Settings;
@ViewChild(MatSort) sort: MatSort;
@ViewChild(MatSort, {static: true}) sort: MatSort;
constructor(
private route: ActivatedRoute,

View File

@ -13,8 +13,9 @@
<ng-container matColumnDef="name">
<mat-header-cell *matHeaderCellDef> Name </mat-header-cell>
<mat-cell *matCellDef="let row">
<a [routerLink]="['/server', row.id, 'projects']" class="table-link">{{ row.name }}</a></mat-cell
>
<a *ngIf="getServerStatus(row) === 'running' || row.location === 'remote' || row.location === 'bundled'" [routerLink]="['/server', row.id, 'projects']" class="table-link">{{ row.name }}</a>
<span *ngIf="getServerStatus(row) != 'running' && row.location !== 'remote' && row.location !== 'bundled'">{{ row.name }}</span>
</mat-cell>
</ng-container>
<ng-container matColumnDef="location">
@ -42,6 +43,8 @@
<button mat-icon-button (click)="stopServer(row)" *ngIf="row.location === 'local' && getServerStatus(row) === 'running'">
<mat-icon aria-label="Stop server">stop</mat-icon>
</button>
<mat-spinner [diameter]="24" *ngIf="row.location === 'local' && getServerStatus(row) === 'starting'"></mat-spinner>
<button mat-icon-button (click)="deleteServer(row)">
<mat-icon aria-label="Remove server">delete</mat-icon>

View File

@ -49,6 +49,9 @@ export class ServersComponent implements OnInit, OnDestroy {
if(!server) {
return;
}
if(serverStatus.status === 'starting') {
server.status = 'starting';
}
if(serverStatus.status === 'stopped') {
server.status = 'stopped';
}

View File

@ -20,7 +20,7 @@ export class TemplateListDialogComponent implements OnInit {
dataSource: TemplateDataSource;
displayedColumns = ['name'];
@ViewChild('filter') filter: ElementRef;
@ViewChild('filter', {static: false}) filter: ElementRef;
constructor(
public dialogRef: MatDialogRef<TemplateListDialogComponent>,

Some files were not shown because too many files have changed in this diff Show More