Merge pull request #1171 from GNS3/Change-link-style

Change link style
This commit is contained in:
Jeremy Grossmann 2022-04-18 14:42:43 +07:00 committed by GitHub
commit 2121d29a9e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 342 additions and 15 deletions

View File

@ -112,6 +112,7 @@ import { DeleteActionComponent } from './components/project-map/context-menu/act
import { DuplicateActionComponent } from './components/project-map/context-menu/actions/duplicate-action/duplicate-action.component';
import { EditConfigActionComponent } from './components/project-map/context-menu/actions/edit-config/edit-config-action.component';
import { EditStyleActionComponent } from './components/project-map/context-menu/actions/edit-style-action/edit-style-action.component';
import { EditLinkStyleActionComponent } from './components/project-map/context-menu/actions/edit-link-style-action/edit-link-style-action.component';
import { EditTextActionComponent } from './components/project-map/context-menu/actions/edit-text-action/edit-text-action.component';
import { ExportConfigActionComponent } from './components/project-map/context-menu/actions/export-config/export-config-action.component';
import { HttpConsoleNewTabActionComponent } from './components/project-map/context-menu/actions/http-console-new-tab/http-console-new-tab-action.component';
@ -138,6 +139,7 @@ import { ContextMenuComponent } from './components/project-map/context-menu/cont
import { ConfigDialogComponent } from './components/project-map/context-menu/dialogs/config-dialog/config-dialog.component';
import { DrawLinkToolComponent } from './components/project-map/draw-link-tool/draw-link-tool.component';
import { StyleEditorDialogComponent } from './components/project-map/drawings-editors/style-editor/style-editor.component';
import { LinkStyleEditorDialogComponent } from './components/project-map/drawings-editors/link-style-editor/link-style-editor.component';
import { TextEditorDialogComponent } from './components/project-map/drawings-editors/text-editor/text-editor.component';
import { HelpDialogComponent } from './components/project-map/help-dialog/help-dialog.component';
import { NodeCreatedLabelStylesFixer } from './components/project-map/helpers/node-created-label-styles-fixer';
@ -303,6 +305,7 @@ import { DeleteAllImageFilesDialogComponent } from './components/image-manager/d
MoveLayerDownActionComponent,
MoveLayerUpActionComponent,
EditStyleActionComponent,
EditLinkStyleActionComponent,
EditTextActionComponent,
DeleteActionComponent,
DuplicateActionComponent,
@ -330,6 +333,7 @@ import { DeleteAllImageFilesDialogComponent } from './components/image-manager/d
InterfaceLabelDraggedComponent,
InstallSoftwareComponent,
StyleEditorDialogComponent,
LinkStyleEditorDialogComponent,
TextEditorDialogComponent,
PacketFiltersDialogComponent,
QemuPreferencesComponent,

View File

@ -5,6 +5,7 @@ import { MatMenuModule } from '@angular/material/menu';
import { ANGULAR_MAP_DECLARATIONS } from './angular-map.imports';
import { D3MapComponent } from './components/d3-map/d3-map.component';
import { DraggableSelectionComponent } from './components/draggable-selection/draggable-selection.component';
import { LinkEditingComponent } from './components/link-editing/link-editing.component';
import { DrawingAddingComponent } from './components/drawing-adding/drawing-adding.component';
import { DrawingResizingComponent } from './components/drawing-resizing/drawing-resizing.component';
import { ExperimentalMapComponent } from './components/experimental-map/experimental-map.component';
@ -73,6 +74,7 @@ import { SerialLinkWidget } from './widgets/links/serial-link';
SelectionControlComponent,
SelectionSelectComponent,
DraggableSelectionComponent,
LinkEditingComponent,
MovingCanvasDirective,
ZoomingCanvasDirective,
],

View File

@ -46,3 +46,4 @@
<app-selection-select></app-selection-select>
<app-text-editor #textEditor [server]="server" [svg]="svg"></app-text-editor>
<app-draggable-selection [svg]="svg"></app-draggable-selection>
<app-link-editing [svg]="svg"></app-link-editing>

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,31 @@
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { select } from 'd3-selection';
import { Subscription } from 'rxjs';
import { LinksEventSource } from '../../events/links-event-source';
import { MapLink } from '../../models/map/map-link';
import { LinksWidget } from '../../widgets/links';
@Component({
selector: 'app-link-editing',
templateUrl: './link-editing.component.html',
styleUrls: ['./link-editing.component.scss'],
})
export class LinkEditingComponent implements OnInit, OnDestroy {
private linkEditedSubscription: Subscription;
@Input('svg') svg: SVGSVGElement;
constructor(
private linksWidget: LinksWidget,
private linksEventSource: LinksEventSource ) {}
ngOnInit() {
const svg = select(this.svg);
this.linkEditedSubscription = this.linksEventSource.edited.subscribe((link: MapLink) => {
this.linksWidget.redrawLink(svg, link);
});
}
ngOnDestroy() {
this.linkEditedSubscription.unsubscribe();
}
}

View File

@ -15,6 +15,7 @@ export class LinkToMapLinkConverter implements Converter<Link, MapLink> {
mapLink.captureFilePath = link.capture_file_path;
mapLink.capturing = link.capturing;
mapLink.filters = link.filters;
mapLink.link_style = link.link_style;
mapLink.linkType = link.link_type;
mapLink.nodes = link.nodes.map((linkNode) =>
this.linkNodeToMapLinkNode.convert(linkNode, { link_id: link.link_id })

View File

@ -16,6 +16,7 @@ export class MapLinkToLinkConverter implements Converter<MapLink, Link> {
link.capturing = mapLink.capturing;
link.filters = mapLink.filters;
link.link_type = mapLink.linkType;
link.link_style = mapLink.link_style;
link.nodes = mapLink.nodes.map((mapLinkNode) => this.mapLinkNodeToMapLinkNode.convert(mapLinkNode));
link.project_id = mapLink.projectId;
link.suspend = mapLink.suspend;

View File

@ -1,4 +1,5 @@
import { EventEmitter, Injectable } from '@angular/core';
import { MapLink } from '../models/map/map-link';
import { MapLinkNode } from '../models/map/map-link-node';
import { DraggedDataEvent } from './event-source';
import { MapLinkCreated } from './links';
@ -6,5 +7,6 @@ import { MapLinkCreated } from './links';
@Injectable()
export class LinksEventSource {
public created = new EventEmitter<MapLinkCreated>();
public edited = new EventEmitter<MapLink>();
public interfaceDragged = new EventEmitter<DraggedDataEvent<MapLinkNode>>();
}

View File

@ -45,6 +45,7 @@ export class GraphDataManager {
public setLinks(links: Link[]) {
if (links) {
console.log("from set links");
const mapLinks = links.map((l) => this.linkToMapLink.convert(l));
this.mapLinksDataSource.set(mapLinks);
@ -88,6 +89,7 @@ export class GraphDataManager {
private onDataUpdate() {
this.layersManager.clear();
this.layersManager.setNodes(this.getNodes());
console.log(this.getLinks());
this.layersManager.setLinks(this.getLinks());
this.layersManager.setDrawings(this.getDrawings());
}

View File

@ -38,6 +38,7 @@ export class LayersManager {
}
public setLinks(links: MapLink[]) {
console.log('from set links 2');
links
.filter((link: MapLink) => link.source && link.target)
.forEach((link: MapLink) => {

View File

@ -2,6 +2,7 @@ import { Filter } from '../../../models/filter';
import { Indexed } from '../../datasources/map-datasource';
import { MapLinkNode } from './map-link-node';
import { MapNode } from './map-node';
import { LinkStyle } from '../../../models/link-style';
export class MapLink implements Indexed {
id: string;
@ -13,6 +14,7 @@ export class MapLink implements Indexed {
nodes: MapLinkNode[];
projectId: string;
suspend: boolean;
link_style?: LinkStyle;
distance: number; // this is not from server
length: number; // this is not from server

View File

@ -7,6 +7,7 @@ export class MapChangeDetectorRef {
public hasBeenDrawn = false;
public detectChanges() {
console.log('from map change detector');
this.changesDetected.emit(true);
}
}

View File

@ -29,7 +29,8 @@ export class LinkWidget implements Widget {
const link_body_enter = link_body.enter().append<SVGGElement>('g').attr('class', 'link_body');
const link_body_merge = link_body.merge(link_body_enter).attr('transform', (link) => {
const link_body_merge = link_body.merge(link_body_enter)
.attr('transform', (link) => {
const translation = this.multiLinkCalculatorHelper.linkTranslation(link.distance, link.source, link.target);
return `translate (${translation.dx}, ${translation.dy})`;
});

View File

@ -11,6 +11,7 @@ export class LinksWidget implements Widget {
constructor(private multiLinkCalculatorHelper: MultiLinkCalculatorHelper, private linkWidget: LinkWidget) {}
public redrawLink(view: SVGSelection, link: MapLink) {
console.log('redraw called');
this.linkWidget.draw(this.selectLink(view, link));
}

View File

@ -4,21 +4,29 @@ import { LinkContextMenu } from '../../events/event-source';
import { MapLink } from '../../models/map/map-link';
import { SVGSelection } from '../../models/types';
import { Widget } from '../widget';
import { LinkStyle } from '../../../models/link-style';
import { StyleTranslator} from './style-translator';
class EthernetLinkPath {
constructor(public source: [number, number], public target: [number, number]) {}
constructor(public source: [number, number], public target: [number, number], public style: LinkStyle) {}
}
@Injectable()
export class EthernetLinkWidget implements Widget {
public onContextMenu = new EventEmitter<LinkContextMenu>();
private defaultEthernetLinkStyle : LinkStyle = {
color: "#000",
width: 2,
type: 0
};
constructor() {}
private linktoEthernetLink(link: MapLink) {
return new EthernetLinkPath(
[link.source.x + link.source.width / 2, link.source.y + link.source.height / 2],
[link.target.x + link.target.width / 2, link.target.y + link.target.height / 2]
[link.target.x + link.target.width / 2, link.target.y + link.target.height / 2],
link.link_style.color ? link.link_style : this.defaultEthernetLinkStyle
);
}
@ -38,15 +46,15 @@ export class EthernetLinkWidget implements Widget {
let link: MapLink = (datum as unknown) as MapLink;
const evt = event;
this.onContextMenu.emit(new LinkContextMenu(evt, link));
});
link_enter
.attr('stroke', '#000')
.attr('stroke-width', '2')
.on('contextmenu', (datum) => {
let link: MapLink = (datum as unknown) as MapLink;
const evt = event;
this.onContextMenu.emit(new LinkContextMenu(evt, link));
})
.attr('stroke', (datum) => {
return datum.style.color;
})
.attr('stroke-width', (datum) => {
return datum.style.width;
})
.attr('stroke-dasharray', (datum) => {
return StyleTranslator.getLinkStyle(datum.style);
});
const link_merge = link.merge(link_enter);

View File

@ -4,19 +4,27 @@ import { LinkContextMenu } from '../../events/event-source';
import { MapLink } from '../../models/map/map-link';
import { SVGSelection } from '../../models/types';
import { Widget } from '../widget';
import { LinkStyle } from '../../../models/link-style';
import { StyleTranslator} from './style-translator';
class SerialLinkPath {
constructor(
public source: [number, number],
public source_angle: [number, number],
public target_angle: [number, number],
public target: [number, number]
public target: [number, number],
public style: LinkStyle
) {}
}
@Injectable()
export class SerialLinkWidget implements Widget {
public onContextMenu = new EventEmitter<LinkContextMenu>();
private defaultSerialLinkStyle : LinkStyle = {
color: "#B22222",
width: 2,
type: 0
};
constructor() {}
@ -47,7 +55,12 @@ export class SerialLinkWidget implements Widget {
target.y - dy / 2.0 - 15 * vect_rot[1],
];
return new SerialLinkPath([source.x, source.y], angle_source, angle_target, [target.x, target.y]);
return new SerialLinkPath(
[source.x, source.y],
angle_source,
angle_target,
[target.x, target.y],
link.link_style.color ? link.link_style : this.defaultSerialLinkStyle);
}
public draw(view: SVGSelection) {
@ -68,7 +81,16 @@ export class SerialLinkWidget implements Widget {
this.onContextMenu.emit(new LinkContextMenu(evt, link));
});
link_enter.attr('stroke', '#B22222').attr('fill', 'none').attr('stroke-width', '2');
link_enter
.attr('stroke', (datum) => {
return datum.style.color;
})
.attr('stroke-width', (datum) => {
return datum.style.width;
})
.attr('stroke-dasharray', (datum) => {
return StyleTranslator.getLinkStyle(datum.style);
});
const link_merge = link.merge(link_enter);

View File

@ -0,0 +1,16 @@
import { LinkStyle } from '../../../models/link-style';
export class StyleTranslator {
static getLinkStyle(linkStyle: LinkStyle) {
if (linkStyle.type == 1) {
return `10, 10`
}
if (linkStyle.type == 2) {
return `${linkStyle.width}, ${linkStyle.width}`
}
if (linkStyle.type == 3) {
return `20, 10, ${linkStyle.width}, ${linkStyle.width}, ${linkStyle.width}, 10`
}
return `0, 0`
}
}

View File

@ -0,0 +1,4 @@
<button mat-menu-item (click)="editStyle()">
<mat-icon>style</mat-icon>
<span>Edit style</span>
</button>

View File

@ -0,0 +1,33 @@
import { Component, Input, OnChanges } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { Link } from '../../../../../models/link';
import { Project } from '../../../../../models/project';
import { Server } from '../../../../../models/server';
import { LinkStyleEditorDialogComponent } from '../../../drawings-editors/link-style-editor/link-style-editor.component';
@Component({
selector: 'app-edit-link-style-action',
templateUrl: './edit-link-style-action.component.html',
})
export class EditLinkStyleActionComponent implements OnChanges {
@Input() server: Server;
@Input() project: Project;
@Input() link: Link;
constructor(private dialog: MatDialog) {}
ngOnChanges() {}
editStyle() {
const dialogRef = this.dialog.open(LinkStyleEditorDialogComponent, {
width: '800px',
autoFocus: false,
disableClose: true,
});
let instance = dialogRef.componentInstance;
instance.server = this.server;
instance.project = this.project;
instance.link = this.link;
}
}

View File

@ -180,6 +180,18 @@
[server]="server"
[link]="links[0]"
></app-reset-link-action>
<app-edit-link-style-action
*ngIf="
!projectService.isReadOnly(project) &&
drawings.length === 0 &&
nodes.length === 0 &&
links.length === 1 &&
linkNodes.length === 0
"
[server]="server"
[project]="project"
[link]="links[0]"
></app-edit-link-style-action>
<app-lock-action
*ngIf="!projectService.isReadOnly(project) && (drawings.length > 0 || nodes.length > 0)"
[server]="server"

View File

@ -0,0 +1,33 @@
<h1 mat-dialog-title>Style editor</h1>
<div class="modal-form-container">
<form [formGroup]="formGroup">
<mat-form-field class="form-field">
<input
matInput
placeholder="Color"
formControlName="color"
type="color"
/>
</mat-form-field>
<mat-form-field class="form-field">
<input
matInput
formControlName="width"
placeholder="Width"
type="number" />
</mat-form-field>
<mat-form-field class="form-field">
<mat-select placeholder="Type" formControlName="type">
<mat-option *ngFor="let type of borderTypes" [value]="type"> {{ type }} </mat-option>
</mat-select>
</mat-form-field>
</form>
</div>
<div mat-dialog-actions>
<button mat-button (click)="onNoClick()" color="accent">Cancel</button>
<button mat-button (click)="onYesClick()" tabindex="2" mat-raised-button color="primary">Apply</button>
</div>

View File

@ -0,0 +1,54 @@
.item {
height: 25px;
font-size: 10pt;
margin-bottom: 10px;
}
.item-name {
margin-bottom: 10px;
}
.item-value {
width: 100%;
margin-bottom: 10px;
}
.input-color {
padding: 0px;
border-width: 0px;
width: 100%;
background-color: transparent;
outline: none;
}
input:focus {
outline: none;
}
input[type='color'] {
-webkit-appearance: none;
border: none;
height: 25px;
}
input[type='color']::-webkit-color-swatch-wrapper {
padding: 0;
}
input[type='color']::-webkit-color-swatch {
border: none;
}
.modal-form-container {
display: flex;
flex-direction: column;
}
.modal-form-container > * {
width: 100%;
}
.form-field {
width: 100%;
}

View File

@ -0,0 +1,83 @@
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { MatDialogRef } from '@angular/material/dialog';
import { Link } from '../../../../models/link';
import { Project } from '../../../../models/project';
import { Server } from '../../../../models/server';
import { ToasterService } from '../../../../services/toaster.service';
import { NonNegativeValidator } from '../../../../validators/non-negative-validator';
import { LinkService } from '../../../../services/link.service';
import { LinksDataSource } from '../../../../cartography/datasources/links-datasource';
import { LinksEventSource } from '../../../../cartography/events/links-event-source';
import { LinkToMapLinkConverter } from '../../../../cartography/converters/map/link-to-map-link-converter';
@Component({
selector: 'app-link-style-editor',
templateUrl: './link-style-editor.component.html',
styleUrls: ['./link-style-editor.component.scss'],
})
export class LinkStyleEditorDialogComponent implements OnInit {
server: Server;
project: Project;
link: Link;
formGroup: FormGroup;
borderTypes = ["Solid", "Dash", "Dot", "Dash Dot"];
constructor(
public dialogRef: MatDialogRef<LinkStyleEditorDialogComponent>,
private formBuilder: FormBuilder,
private toasterService: ToasterService,
private linkService: LinkService,
private linksDataSource: LinksDataSource,
private linksEventSource: LinksEventSource,
private linkToMapLink: LinkToMapLinkConverter,
private nonNegativeValidator: NonNegativeValidator
) {
this.formGroup = this.formBuilder.group({
color: new FormControl('', [Validators.required]),
width: new FormControl('', [Validators.required, nonNegativeValidator.get]),
type: new FormControl('', [Validators.required])
});
}
ngOnInit() {
if (!this.link.link_style?.color) {
this.formGroup.controls['color'].setValue("#000000");
} else {
this.formGroup.controls['color'].setValue(this.link.link_style.color);
}
this.formGroup.controls['width'].setValue(this.link.link_style.width);
let type = this.borderTypes[0];
if (this.link.link_style?.type) {
type = this.borderTypes[this.link.link_style.type];
}
this.formGroup.controls['type'].setValue(type);
}
onNoClick() {
this.dialogRef.close();
}
onYesClick() {
if (this.formGroup.valid) {
this.link.link_style.color = this.formGroup.get('color').value;
this.link.link_style.width = this.formGroup.get('width').value;
let type = this.borderTypes.indexOf(this.formGroup.get('type').value);
this.link.link_style.type = type;
this.linkService.updateLinkStyle(this.server, this.link).subscribe((link) => {
this.linksDataSource.update(link);
this.linksEventSource.edited.next(this.linkToMapLink.convert(link));
location.reload()
// we add this code/line for reload the entire page because single graph/link style is not updated automatically.
// this.toasterService.success("Link updated");
this.dialogRef.close();
});
} else {
this.toasterService.error(`Entered data is incorrect`);
}
}
}

View File

@ -268,6 +268,7 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
this.projectMapSubscription.add(
this.linksDataSource.changes.subscribe((links: Link[]) => {
console.log('from project map component');
this.links = links;
this.mapChangeDetectorRef.detectChanges();
})

View File

@ -0,0 +1,5 @@
export class LinkStyle {
color: string;
width: number;
type: number;
}

View File

@ -1,6 +1,7 @@
import { Node } from '../cartography/models/node';
import { Filter } from './filter';
import { LinkNode } from './link-node';
import { LinkStyle } from './link-style';
export class Link {
capture_file_name: string;
@ -12,6 +13,7 @@ export class Link {
nodes: LinkNode[];
project_id: string;
suspend: boolean;
link_style?: LinkStyle;
distance: number; // this is not from server
length: number; // this is not from server

View File

@ -69,6 +69,10 @@ export class LinkService {
return this.httpServer.put<Link>(server, `/projects/${link.project_id}/links/${link.link_id}`, link);
}
updateLinkStyle(server: Server, link: Link) {
return this.httpServer.put<Link>(server, `/projects/${link.project_id}/links/${link.link_id}`, link);
}
getAvailableFilters(server: Server, link: Link) {
return this.httpServer.get<FilterDescription[]>(
server,