diff --git a/package.json b/package.json index 8b7328e6..cea7947b 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "d3-ng2-service": "^2.1.0", "file-saver": "^2.0.2", "ini": "^1.3.5", + "marked": "^1.1.1", "material-design-icons": "^3.0.1", "ng-circle-progress": "^1.6.0", "ng2-file-upload": "^1.3.0", diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 8c564075..306c4244 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -281,9 +281,12 @@ import { ChangeHostnameActionComponent } from './components/project-map/context- import { ChangeHostnameDialogComponent } from './components/project-map/change-hostname-dialog/change-hostname-dialog.component'; import { ApplianceInfoDialogComponent } from './components/project-map/new-template-dialog/appliance-info-dialog/appliance-info-dialog.component'; import { ResetLinkActionComponent } from './components/project-map/context-menu/actions/reset-link/reset-link-action.component'; +import { ReadmeEditorComponent } from './components/projects/edit-project-dialog/readme-editor/readme-editor.component'; +import { MarkedDirective } from './directives/marked.directive'; import { InformationDialogComponent } from './components/dialogs/information-dialog.component'; import { TemplateNameDialogComponent } from './components/project-map/new-template-dialog/template-name-dialog/template-name-dialog.component'; import { UpdatesService } from './services/updates.service'; +import { ProjectReadmeComponent } from './components/project-map/project-readme/project-readme.component'; @NgModule({ @@ -470,6 +473,8 @@ import { UpdatesService } from './services/updates.service'; ChangeHostnameActionComponent, ChangeHostnameDialogComponent, ApplianceInfoDialogComponent, + ReadmeEditorComponent, + MarkedDirective, InformationDialogComponent, TemplateNameDialogComponent, ConfigureCustomAdaptersDialogComponent, @@ -617,7 +622,8 @@ import { UpdatesService } from './services/updates.service'; ChangeHostnameDialogComponent, ApplianceInfoDialogComponent, ConfigureCustomAdaptersDialogComponent, - EditNetworkConfigurationDialogComponent + EditNetworkConfigurationDialogComponent, + ProjectReadmeComponent ], bootstrap: [AppComponent] }) diff --git a/src/app/components/project-map/project-map.component.ts b/src/app/components/project-map/project-map.component.ts index f1374782..ca3a2f0c 100644 --- a/src/app/components/project-map/project-map.component.ts +++ b/src/app/components/project-map/project-map.component.ts @@ -72,6 +72,7 @@ import { ThemeService } from '../../services/theme.service'; import { Title } from '@angular/platform-browser'; import { NewTemplateDialogComponent } from './new-template-dialog/new-template-dialog.component'; import { NodeConsoleService } from '../../services/nodeConsole.service'; +import { ProjectReadmeComponent } from './project-readme/project-readme.component'; @Component({ @@ -258,8 +259,7 @@ export class ProjectMapComponent implements OnInit, OnDestroy { if (!project ) this.router.navigate(['/servers']); this.projectService.open(this.server, this.project.project_id); - this.title.setTitle(this.project.name); - + this.title.setTitle(this.project.name); this.isInterfaceLabelVisible = this.mapSettingsService.showInterfaceLabels; this.recentlyOpenedProjectService.setServerId(this.server.id.toString()); @@ -277,6 +277,7 @@ export class ProjectMapComponent implements OnInit, OnDestroy { .subscribe( (project: Project) => { this.onProjectLoad(project); + if (this.mapSettingsService.openReadme) this.showReadme(); }, error => { this.progressService.setError(error); @@ -897,6 +898,18 @@ export class ProjectMapComponent implements OnInit, OnDestroy { instance.project = this.project; } + public showReadme() { + const dialogRef = this.dialog.open(ProjectReadmeComponent, { + width: '600px', + height: '650px', + autoFocus: false, + disableClose: true + }); + let instance = dialogRef.componentInstance; + instance.server = this.server; + instance.project = this.project; + } + public ngOnDestroy() { this.nodeConsoleService.openConsoles = 0; this.title.setTitle('GNS3 Web UI'); diff --git a/src/app/components/project-map/project-readme/project-readme.component.html b/src/app/components/project-map/project-readme/project-readme.component.html new file mode 100644 index 00000000..263e05cf --- /dev/null +++ b/src/app/components/project-map/project-readme/project-readme.component.html @@ -0,0 +1,7 @@ +<h1 mat-dialog-title>Project README</h1> + +<div class="textWrapper" id="text"></div> + +<div mat-dialog-actions> + <button mat-button (click)="onNoClick()" color="accent">Close</button> +</div> \ No newline at end of file diff --git a/src/app/components/project-map/project-readme/project-readme.component.scss b/src/app/components/project-map/project-readme/project-readme.component.scss new file mode 100644 index 00000000..605a3657 --- /dev/null +++ b/src/app/components/project-map/project-readme/project-readme.component.scss @@ -0,0 +1,4 @@ +.textWrapper { + height: 500px!important; + overflow-y: scroll; +} diff --git a/src/app/components/project-map/project-readme/project-readme.component.ts b/src/app/components/project-map/project-readme/project-readme.component.ts new file mode 100644 index 00000000..ba3bdb5e --- /dev/null +++ b/src/app/components/project-map/project-readme/project-readme.component.ts @@ -0,0 +1,45 @@ +import { Component, AfterViewInit } from '@angular/core'; +import { MatDialogRef } from '@angular/material/dialog'; +import { Server } from '../../../models/server'; +import { Project } from '../../../models/project'; +import { ProjectService } from '../../../services/project.service'; +import * as marked from 'marked'; +import { ElementRef } from '@angular/core'; +import { Renderer2 } from '@angular/core'; +import { ViewChild } from '@angular/core'; + +@Component({ + selector: 'app-project-readme', + templateUrl: './project-readme.component.html', + styleUrls: ['./project-readme.component.scss'] +}) +export class ProjectReadmeComponent implements AfterViewInit { + server: Server; + project: Project; + @ViewChild('text', {static: false}) text: ElementRef; + + constructor( + public dialogRef: MatDialogRef<ProjectReadmeComponent>, + private projectService: ProjectService, + private elementRef: ElementRef, + private renderer: Renderer2 + ) {} + + ngAfterViewInit() { + let markdown = ``; + + this.projectService.getReadmeFile(this.server, this.project.project_id).subscribe(file => { + if (file) { + markdown = file; + setTimeout(function(){ + const markdownHtml = marked(markdown); + document.getElementById('text').innerHTML = markdownHtml; + }, 1000); + } + }); + } + + onNoClick() { + this.dialogRef.close(); + } +} diff --git a/src/app/components/projects/edit-project-dialog/edit-project-dialog.component.html b/src/app/components/projects/edit-project-dialog/edit-project-dialog.component.html index 392bbbdb..2acfd347 100644 --- a/src/app/components/projects/edit-project-dialog/edit-project-dialog.component.html +++ b/src/app/components/projects/edit-project-dialog/edit-project-dialog.component.html @@ -41,6 +41,11 @@ Show interface labels at start </mat-checkbox> </mat-tab> + + <mat-tab label="Readme" *ngIf="server && project"> + <app-readme-editor #editor [server]="server" [project]="project"></app-readme-editor> + </mat-tab> + <mat-tab label="Global variables"> <form [formGroup]="variableFormGroup"> <mat-form-field class="form-field"> diff --git a/src/app/components/projects/edit-project-dialog/edit-project-dialog.component.ts b/src/app/components/projects/edit-project-dialog/edit-project-dialog.component.ts index e08e09ae..1cbb3753 100644 --- a/src/app/components/projects/edit-project-dialog/edit-project-dialog.component.ts +++ b/src/app/components/projects/edit-project-dialog/edit-project-dialog.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, Injectable } from '@angular/core'; +import { Component, OnInit, Injectable, ViewChild } from '@angular/core'; import { MatDialogRef } from '@angular/material/dialog'; import { FormGroup, FormBuilder, FormControl, Validators } from '@angular/forms'; import { Server } from '../../../models/server'; @@ -6,6 +6,7 @@ import { Project, ProjectVariable } from '../../../models/project'; import { ToasterService } from '../../../services/toaster.service'; import { NonNegativeValidator } from '../../../validators/non-negative-validator'; import { ProjectService } from '../../../services/project.service'; +import { ReadmeEditorComponent } from './readme-editor/readme-editor.component'; @Component({ selector: 'app-edit-project-dialog', @@ -13,6 +14,8 @@ import { ProjectService } from '../../../services/project.service'; styleUrls: ['./edit-project-dialog.component.scss'] }) export class EditProjectDialogComponent implements OnInit { + @ViewChild('editor') editor: ReadmeEditorComponent; + server: Server; project: Project; formGroup: FormGroup; @@ -89,8 +92,10 @@ export class EditProjectDialogComponent implements OnInit { this.project.auto_close = !this.project.auto_close; this.projectService.update(this.server, this.project).subscribe((project: Project) => { - this.toasterService.success(`Project ${project.name} updated.`); - this.onNoClick(); + this.projectService.postReadmeFile(this.server, this.project.project_id, this.editor.markdown).subscribe((response) => { + this.toasterService.success(`Project ${project.name} updated.`); + this.onNoClick(); + }); }) } else { this.toasterService.error(`Fill all required fields with correct values.`); diff --git a/src/app/components/projects/edit-project-dialog/readme-editor/readme-editor.component.html b/src/app/components/projects/edit-project-dialog/readme-editor/readme-editor.component.html new file mode 100644 index 00000000..ee508436 --- /dev/null +++ b/src/app/components/projects/edit-project-dialog/readme-editor/readme-editor.component.html @@ -0,0 +1,11 @@ +<mat-tab-group> + <mat-tab label="Edit"> + <textarea class="editorWrapper" matInput type="text" [(ngModel)]="markdown"></textarea> + </mat-tab> + + <mat-tab label="Preview"> + <span *ngIf="markdown"> + <div class="textWrapper" appMarked text={{markdown}}></div> + </span> + </mat-tab> +</mat-tab-group> diff --git a/src/app/components/projects/edit-project-dialog/readme-editor/readme-editor.component.scss b/src/app/components/projects/edit-project-dialog/readme-editor/readme-editor.component.scss new file mode 100644 index 00000000..ea88fb98 --- /dev/null +++ b/src/app/components/projects/edit-project-dialog/readme-editor/readme-editor.component.scss @@ -0,0 +1,11 @@ +.textWrapper { + height: 500px!important; + overflow-y: scroll; +} + +.editorWrapper { + background-color: white; + color: black; + height: 500px!important; + overflow-y: scroll; +} diff --git a/src/app/components/projects/edit-project-dialog/readme-editor/readme-editor.component.ts b/src/app/components/projects/edit-project-dialog/readme-editor/readme-editor.component.ts new file mode 100644 index 00000000..6edf5950 --- /dev/null +++ b/src/app/components/projects/edit-project-dialog/readme-editor/readme-editor.component.ts @@ -0,0 +1,28 @@ +import { Component, OnInit, ViewEncapsulation, Input } from '@angular/core'; +import * as marked from 'marked'; +import { ProjectService } from '../../../../services/project.service'; +import { Server } from '../../../../models/server'; +import { Project } from '../../../../models/project'; + +@Component({ + selector: 'app-readme-editor', + templateUrl: './readme-editor.component.html', + encapsulation: ViewEncapsulation.None, + styleUrls: ['./readme-editor.component.scss'] +}) +export class ReadmeEditorComponent implements OnInit { + @Input() server: Server; + @Input() project: Project; + + public markdown = ``; + + constructor( + private projectService: ProjectService + ) {} + + ngOnInit() { + this.projectService.getReadmeFile(this.server, this.project.project_id).subscribe(file => { + if (file) this.markdown = file; + }); + } +} diff --git a/src/app/components/settings/settings.component.html b/src/app/components/settings/settings.component.html index 1c3c0b6c..ea5a4aa2 100644 --- a/src/app/components/settings/settings.component.html +++ b/src/app/components/settings/settings.component.html @@ -11,7 +11,8 @@ <div> <mat-checkbox [(ngModel)]="settings.crash_reports">Send anonymous crash reports</mat-checkbox><br/> - <mat-checkbox [(ngModel)]="integrateLinksLabelsToLinks">Integrate link labels to links</mat-checkbox> + <mat-checkbox [(ngModel)]="integrateLinksLabelsToLinks">Integrate link labels to links</mat-checkbox><br/> + <mat-checkbox [(ngModel)]="openReadme">Automatically open project README files</mat-checkbox> </div> <!-- <div> diff --git a/src/app/components/settings/settings.component.ts b/src/app/components/settings/settings.component.ts index 5ff17c4f..62b2d524 100644 --- a/src/app/components/settings/settings.component.ts +++ b/src/app/components/settings/settings.component.ts @@ -15,6 +15,7 @@ export class SettingsComponent implements OnInit { settings = { ...SettingsService.DEFAULTS }; consoleCommand: string; integrateLinksLabelsToLinks: boolean; + openReadme: boolean; constructor( private settingsService: SettingsService, @@ -29,12 +30,15 @@ export class SettingsComponent implements OnInit { this.settings = this.settingsService.getAll(); this.consoleCommand = this.consoleService.command; this.integrateLinksLabelsToLinks = this.mapSettingsService.integrateLinkLabelsToLinks; + this.openReadme = this.mapSettingsService.openReadme; } save() { this.settingsService.setAll(this.settings); this.toaster.success('Settings have been saved.'); + this.mapSettingsService.toggleIntegrateInterfaceLabels(this.integrateLinksLabelsToLinks); + this.mapSettingsService.toggleOpenReadme(this.openReadme); } setDarkMode(value: boolean) { diff --git a/src/app/directives/marked.directive.ts b/src/app/directives/marked.directive.ts new file mode 100644 index 00000000..aece1de7 --- /dev/null +++ b/src/app/directives/marked.directive.ts @@ -0,0 +1,29 @@ +import { Directive, ElementRef, OnInit, Renderer2, Input, OnChanges } from '@angular/core'; +import * as marked from 'marked'; + +@Directive({ + selector: '[appMarked]' +}) +export class MarkedDirective implements OnInit, OnChanges { + @Input() text: string; + + constructor(private elementRef: ElementRef, + private renderer: Renderer2) { } + + ngOnInit() { + this.updateText(); + } + + ngOnChanges() { + this.updateText(); + } + + updateText() { + const markText = this.text; + + if (markText && markText.length > 0) { + const markdownHtml = marked(markText); + this.renderer.setProperty(this.elementRef.nativeElement, 'innerHTML', markdownHtml); + } + } +} diff --git a/src/app/services/mapsettings.service.ts b/src/app/services/mapsettings.service.ts index 6b169ae0..cda8874c 100644 --- a/src/app/services/mapsettings.service.ts +++ b/src/app/services/mapsettings.service.ts @@ -13,10 +13,12 @@ export class MapSettingsService { public showInterfaceLabels: boolean = true; public integrateLinkLabelsToLinks: boolean = true; + public openReadme: boolean = true; constructor() { this.isLayerNumberVisible = localStorage.getItem('layersVisibility') === 'true' ? true : false; if (localStorage.getItem('integrateLinkLabelsToLinks')) this.integrateLinkLabelsToLinks = localStorage.getItem('integrateLinkLabelsToLinks') === 'true' ? true : false; + if (localStorage.getItem('openReadme')) this.openReadme = localStorage.getItem('openReadme') === 'true' ? true : false; } changeMapLockValue(value: boolean) { @@ -56,4 +58,14 @@ export class MapSettingsService { localStorage.setItem('integrateLinkLabelsToLinks', 'false'); } } + + toggleOpenReadme(value: boolean) { + this.openReadme = value; + localStorage.removeItem('openReadme'); + if (value) { + localStorage.setItem('openReadme', 'true'); + } else { + localStorage.setItem('openReadme', 'false'); + } + } } diff --git a/src/app/services/project.service.ts b/src/app/services/project.service.ts index f4a1057c..682766a0 100644 --- a/src/app/services/project.service.ts +++ b/src/app/services/project.service.ts @@ -18,6 +18,14 @@ export class ProjectService { this.projectListSubject.next(true); } + getReadmeFile(server: Server, project_id: string) { + return this.httpServer.get<any>(server, `/projects/${project_id}/files/README.txt`); + } + + postReadmeFile(server: Server, project_id: string, readme: string) { + return this.httpServer.post<any>(server, `/projects/${project_id}/files/README.txt`, readme); + } + get(server: Server, project_id: string) { return this.httpServer.get<Project>(server, `/projects/${project_id}`); } diff --git a/yarn.lock b/yarn.lock index c587f6f2..f56c94e6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7684,6 +7684,11 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" +marked@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/marked/-/marked-1.1.1.tgz#e5d61b69842210d5df57b05856e0c91572703e6a" + integrity sha512-mJzT8D2yPxoPh7h0UXkB+dBj4FykPJ2OIfxAWeIHrvoHDkFxukV/29QxoFQoPM6RLEwhIFdJpmKBlqVM3s2ZIw== + matcher@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/matcher/-/matcher-3.0.0.tgz#bd9060f4c5b70aa8041ccc6f80368760994f30ca"