mirror of
https://github.com/GNS3/gns3-web-ui.git
synced 2024-12-21 05:53:06 +00:00
Merge branch 'master' into custom-html-adbutler-ads
This commit is contained in:
commit
ebbeac6e0b
@ -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:
|
||||
|
@ -8,6 +8,11 @@ jobs:
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
- run:
|
||||
name: Install nodejs
|
||||
command: |
|
||||
brew upgrade node
|
||||
|
||||
- run:
|
||||
name: Set timezone and check current datetime
|
||||
command: |
|
||||
|
@ -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
|
||||
|
19
README.md
19
README.md
@ -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
|
||||
|
||||
|
@ -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",
|
||||
|
82
package.json
82
package.json
@ -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": [
|
||||
|
@ -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
16
src/ReleaseNotes.txt
Normal 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
|
@ -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 },
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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 |
@ -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'])
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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[] = [];
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
|
@ -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[];
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
169
src/app/cartography/directives/moving-canvas.directive.spec.ts
Normal file
169
src/app/cartography/directives/moving-canvas.directive.spec.ts
Normal 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)`);
|
||||
}));
|
||||
});
|
65
src/app/cartography/directives/moving-canvas.directive.ts
Normal file
65
src/app/cartography/directives/moving-canvas.directive.ts
Normal 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);
|
||||
}
|
||||
}
|
139
src/app/cartography/directives/zooming-canvas.directive.spec.ts
Normal file
139
src/app/cartography/directives/zooming-canvas.directive.spec.ts
Normal 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();
|
||||
}));
|
||||
});
|
61
src/app/cartography/directives/zooming-canvas.directive.ts
Normal file
61
src/app/cartography/directives/zooming-canvas.directive.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -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) {}
|
||||
}
|
||||
|
6
src/app/cartography/events/moving-event-source.ts
Normal file
6
src/app/cartography/events/moving-event-source.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { Injectable, EventEmitter } from "@angular/core";
|
||||
|
||||
@Injectable()
|
||||
export class MovingEventSource {
|
||||
public movingModeState = new EventEmitter<boolean>();
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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')
|
||||
|
@ -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)
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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() {
|
||||
|
@ -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(
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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) => {
|
||||
|
21
src/app/components/help/help.component.html
Normal file
21
src/app/components/help/help.component.html
Normal 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>
|
0
src/app/components/help/help.component.scss
Normal file
0
src/app/components/help/help.component.scss
Normal file
0
src/app/components/help/help.component.spec.ts
Normal file
0
src/app/components/help/help.component.spec.ts
Normal file
33
src/app/components/help/help.component.ts
Normal file
33
src/app/components/help/help.component.ts
Normal 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 />")
|
||||
});
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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');
|
||||
|
@ -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}`,
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
|
@ -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"/>
|
||||
|
@ -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";
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
@ -29,7 +29,7 @@ export class VirtualBoxTemplateDetailsComponent implements OnInit {
|
||||
generalSettingsForm: FormGroup;
|
||||
networkForm: FormGroup
|
||||
|
||||
@ViewChild("customAdaptersConfigurator")
|
||||
@ViewChild("customAdaptersConfigurator", {static: false})
|
||||
customAdaptersConfigurator: CustomAdaptersComponent;
|
||||
|
||||
constructor(
|
||||
|
@ -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,
|
||||
|
@ -28,7 +28,7 @@ export class VmwareTemplateDetailsComponent implements OnInit {
|
||||
onCloseOptions = [];
|
||||
networkTypes = [];
|
||||
|
||||
@ViewChild("customAdaptersConfigurator")
|
||||
@ViewChild("customAdaptersConfigurator", {static: false})
|
||||
customAdaptersConfigurator: CustomAdaptersComponent;
|
||||
|
||||
constructor(
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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 () => {
|
||||
|
@ -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');
|
||||
|
@ -0,0 +1,4 @@
|
||||
<button mat-menu-item (click)="duplicate()">
|
||||
<mat-icon>filter_none</mat-icon>
|
||||
<span>Duplicate</span>
|
||||
</button>
|
@ -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();
|
||||
});
|
||||
});
|
@ -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}`);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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>
|
||||
|
@ -47,3 +47,7 @@ input[type="color"]::-webkit-color-swatch {
|
||||
.modal-form-container > * {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -52,3 +52,7 @@ input[type="color"]::-webkit-color-swatch {
|
||||
.modal-form-container > * {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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>
|
@ -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;
|
||||
}
|
@ -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);
|
||||
});
|
||||
});
|
@ -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() {}
|
||||
}
|
@ -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>
|
||||
|
@ -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 {
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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';
|
||||
}
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user