mirror of
https://github.com/GNS3/gns3-web-ui.git
synced 2024-12-30 01:38:50 +00:00
Merge branch 'master-3.0' into Add-navigation-to-user-management-page
This commit is contained in:
commit
717336fbd1
16
angular.json
16
angular.json
@ -18,7 +18,6 @@
|
||||
"css-tree",
|
||||
"save-svg-as-png",
|
||||
"angular-draggable-droppable",
|
||||
"angular2-hotkeys",
|
||||
"dom-set",
|
||||
"dom-plane",
|
||||
"mousetrap",
|
||||
@ -26,13 +25,9 @@
|
||||
"rxjs/Rx",
|
||||
"rxjs/add/operator/map",
|
||||
"rxjs-compat/add/operator/map",
|
||||
"angular-react-core.js",
|
||||
"react",
|
||||
"react-dom",
|
||||
"classnames",
|
||||
"stylenames"
|
||||
],
|
||||
"aot": true,
|
||||
"outputPath": "dist",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
@ -49,7 +44,13 @@
|
||||
"src/styles.scss",
|
||||
"src/theme.scss"
|
||||
],
|
||||
"scripts": []
|
||||
"scripts": [],
|
||||
"vendorChunk": true,
|
||||
"extractLicenses": false,
|
||||
"buildOptimizer": false,
|
||||
"sourceMap": true,
|
||||
"optimization": false,
|
||||
"namedChunks": true
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
@ -67,7 +68,6 @@
|
||||
"styles": false
|
||||
},
|
||||
"namedChunks": false,
|
||||
"aot": true,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true,
|
||||
@ -89,7 +89,6 @@
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"namedChunks": false,
|
||||
"aot": true,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true,
|
||||
@ -125,7 +124,6 @@
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"namedChunks": false,
|
||||
"aot": true,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true,
|
||||
|
63
package.json
63
package.json
@ -41,39 +41,32 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular-react/core": "^3.0.0",
|
||||
"@angular-react/fabric": "^3.0.0",
|
||||
"@angular/animations": "^11.2.13",
|
||||
"@angular/cdk": "^11.2.12",
|
||||
"@angular/common": "^11.2.13",
|
||||
"@angular/compiler": "^11.2.13",
|
||||
"@angular/core": "^11.2.13",
|
||||
"@angular/forms": "^11.2.13",
|
||||
"@angular/http": "^7.2.16",
|
||||
"@angular/material": "^11.2.12",
|
||||
"@angular/platform-browser": "^11.2.13",
|
||||
"@angular/platform-browser-dynamic": "^11.2.13",
|
||||
"@angular/router": "^11.2.13",
|
||||
"@angular/animations": "^12.0.2",
|
||||
"@angular/cdk": "^12.0.2",
|
||||
"@angular/common": "^12.0.2",
|
||||
"@angular/compiler": "^12.0.2",
|
||||
"@angular/core": "^12.0.2",
|
||||
"@angular/forms": "^12.0.2",
|
||||
"@angular/material": "^12.0.2",
|
||||
"@angular/platform-browser": "^12.0.2",
|
||||
"@angular/platform-browser-dynamic": "^12.0.2",
|
||||
"@angular/router": "^12.0.2",
|
||||
"@sentry/browser": "^6.3.6",
|
||||
"@types/jest": "^26.0.23",
|
||||
"@types/mocha": "^8.2.2",
|
||||
"@types/react": "^17.0.5",
|
||||
"@types/react-dom": "^17.0.3",
|
||||
"angular-draggable-droppable": "^4.6.0",
|
||||
"angular-persistence": "^1.0.1",
|
||||
"angular-resizable-element": "^3.3.5",
|
||||
"angular2-draggable": "^2.3.2",
|
||||
"angular2-hotkeys": "^2.2.0",
|
||||
"angular2-indexeddb": "^1.2.3",
|
||||
"bootstrap": "^5.0.0",
|
||||
"command-exists": "^1.2.9",
|
||||
"core-js": "^3.12.1",
|
||||
"d3-ng2-service": "^2.2.0",
|
||||
"eev": "^0.1.5",
|
||||
"file-saver": "^2.0.5",
|
||||
"ini": "^2.0.0",
|
||||
"marked": "^2.0.3",
|
||||
"material-design-icons": "^3.0.1",
|
||||
"mousetrap": "^1.6.5",
|
||||
"ng-circle-progress": "^1.6.0",
|
||||
"ng2-file-upload": "^1.4.0",
|
||||
"ngx-childprocess": "^0.0.6",
|
||||
@ -81,16 +74,10 @@
|
||||
"ngx-electron": "^2.2.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"notosans-fontface": "1.2.2",
|
||||
"office-ui-fabric-react": "^7.170.0",
|
||||
"prettier-plugin-organize-imports": "^2.0.0",
|
||||
"react": "^17.0.2",
|
||||
"react-bootstrap": "^1.5.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"rxjs": "^6.6.7",
|
||||
"rxjs-compat": "^6.6.7",
|
||||
"save-html-as-image": "^1.5.2",
|
||||
"rxjs": "^6.5.3",
|
||||
"rxjs-compat": "^6.5.3",
|
||||
"save-svg-as-png": "^1.4.17",
|
||||
"schematics-scss-migrate": "^1.3.13",
|
||||
"snyk": "^1.589.0",
|
||||
"spark-md5": "^3.0.1",
|
||||
"svg-crowbar": "^0.6.5",
|
||||
@ -101,21 +88,21 @@
|
||||
"xterm-addon-attach": "^0.6.0",
|
||||
"xterm-addon-fit": "^0.5.0",
|
||||
"yargs": "^17.0.1",
|
||||
"zone.js": "^0.11.4"
|
||||
"zone.js": "~0.11.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^0.1102.12",
|
||||
"@angular/cli": "^11.2.12",
|
||||
"@angular/compiler-cli": "^11.2.13",
|
||||
"@angular/language-service": "^11.2.13",
|
||||
"@angular-devkit/build-angular": "^12.0.2",
|
||||
"@angular/cli": "^12.0.2",
|
||||
"@angular/compiler-cli": "^12.0.2",
|
||||
"@angular/language-service": "^12.0.2",
|
||||
"@sentry/cli": "^1.64.2",
|
||||
"@sentry/electron": "^2.4.1",
|
||||
"@types/jasmine": "^3.7.1",
|
||||
"@types/jasminewd2": "^2.0.9",
|
||||
"@types/node": "15.0.2",
|
||||
"@types/node": "15.6.1",
|
||||
"codelyzer": "^6.0.2",
|
||||
"electron": "^12.0.7",
|
||||
"electron-builder": "22.11.1",
|
||||
"electron": "^13.0.1",
|
||||
"electron-builder": "22.10.5",
|
||||
"file-loader": "^6.2.0",
|
||||
"jasmine-core": "~3.7.1",
|
||||
"jasmine-spec-reporter": "~7.0.0",
|
||||
@ -133,11 +120,11 @@
|
||||
"replace": "^1.2.1",
|
||||
"rxjs-tslint": "^0.1.8",
|
||||
"ts-mockito": "^2.6.1",
|
||||
"ts-node": "~9.1.1",
|
||||
"ts-node": "~10.0.0",
|
||||
"tslint": "^6.1.3",
|
||||
"tslint-config-prettier": "^1.18.0",
|
||||
"typescript": "4.0.2",
|
||||
"webpack": "5.36.2",
|
||||
"typescript": "4.2.4",
|
||||
"webpack": "5.38.0",
|
||||
"yarn-upgrade-all": "^0.5.4"
|
||||
},
|
||||
"greenkeeper": {
|
||||
@ -146,4 +133,4 @@
|
||||
]
|
||||
},
|
||||
"snyk": true
|
||||
}
|
||||
}
|
@ -3,7 +3,6 @@ import { RouterModule, Routes } from '@angular/router';
|
||||
import { BundledServerFinderComponent } from './components/bundled-server-finder/bundled-server-finder.component';
|
||||
import { DirectLinkComponent } from './components/direct-link/direct-link.component';
|
||||
import { HelpComponent } from './components/help/help.component';
|
||||
import { ReportIssueComponent } from './components/help/report-issue/report-issue.component';
|
||||
import { InstalledSoftwareComponent } from './components/installed-software/installed-software.component';
|
||||
import { LoginComponent } from './components/login/login.component';
|
||||
import { PageNotFoundComponent } from './components/page-not-found/page-not-found.component';
|
||||
@ -57,6 +56,7 @@ import { LoginGuard } from './guards/login-guard';
|
||||
import { DefaultLayoutComponent } from './layouts/default-layout/default-layout.component';
|
||||
import { ServerResolve } from './resolvers/server-resolve';
|
||||
import { UserManagementComponent } from './components/user-management/user-management.component';
|
||||
import { LoggedUserComponent } from './components/users/logged-user/logged-user.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@ -67,6 +67,7 @@ const routes: Routes = [
|
||||
{ path: 'servers', component: ServersComponent },
|
||||
{ path: 'bundled', component: BundledServerFinderComponent },
|
||||
{ path: 'server/:server_id/login', component: LoginComponent },
|
||||
{ path: 'server/:server_id/loggeduser', component: LoggedUserComponent },
|
||||
{
|
||||
path: 'server/:server_id/projects',
|
||||
component: ProjectsComponent,
|
||||
@ -74,7 +75,6 @@ const routes: Routes = [
|
||||
resolve: { server: ServerResolve },
|
||||
},
|
||||
{ path: 'help', component: HelpComponent },
|
||||
{ path: 'help/reportissue', component: ReportIssueComponent },
|
||||
{ path: 'settings', component: SettingsComponent },
|
||||
{ path: 'settings/console', component: ConsoleComponent },
|
||||
{ path: 'installed-software', component: InstalledSoftwareComponent },
|
||||
|
@ -2,7 +2,6 @@ import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { PersistenceService } from 'angular-persistence';
|
||||
import { ElectronService, NgxElectronModule } from 'ngx-electron';
|
||||
import { AppComponent } from './app.component';
|
||||
import { ProgressService } from './common/progress/progress.service';
|
||||
@ -21,12 +20,12 @@ describe('AppComponent', () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [AppComponent],
|
||||
imports: [RouterTestingModule, MatIconModule, NgxElectronModule],
|
||||
providers: [SettingsService, PersistenceService, ProgressService],
|
||||
providers: [SettingsService, ProgressService],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
}).compileComponents();
|
||||
|
||||
electronService = TestBed.get(ElectronService);
|
||||
settingsService = TestBed.get(SettingsService);
|
||||
electronService = TestBed.inject(ElectronService);
|
||||
settingsService = TestBed.inject(SettingsService);
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
@ -46,23 +45,18 @@ describe('AppComponent', () => {
|
||||
}));
|
||||
|
||||
it('should receive changed settings and forward to electron', async(() => {
|
||||
const spy = createSpyObj('Electron.IpcRenderer', ['send']);
|
||||
spyOnProperty(electronService, 'isElectronApp').and.returnValue(true);
|
||||
spyOnProperty(electronService, 'ipcRenderer').and.returnValue(spy);
|
||||
settingsService.set('crash_reports', true);
|
||||
settingsService.setReportsSettings(true);
|
||||
component.ngOnInit();
|
||||
settingsService.set('crash_reports', false);
|
||||
expect(spy.send).toHaveBeenCalled();
|
||||
expect(spy.send.calls.mostRecent().args[0]).toEqual('settings.changed');
|
||||
expect(spy.send.calls.mostRecent().args[1].crash_reports).toEqual(false);
|
||||
settingsService.setReportsSettings(false);
|
||||
}));
|
||||
|
||||
it('should receive changed settings and do not forward to electron', async(() => {
|
||||
const spy = createSpyObj('Electron.IpcRenderer', ['send']);
|
||||
spyOnProperty(electronService, 'isElectronApp').and.returnValue(false);
|
||||
settingsService.set('crash_reports', true);
|
||||
settingsService.setReportsSettings(true);
|
||||
component.ngOnInit();
|
||||
settingsService.set('crash_reports', false);
|
||||
settingsService.setReportsSettings(false);
|
||||
expect(spy.send).not.toHaveBeenCalled();
|
||||
}));
|
||||
});
|
||||
|
@ -37,12 +37,6 @@ export class AppComponent implements OnInit {
|
||||
@HostBinding('class') componentCssClass;
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.electronService.isElectronApp) {
|
||||
this.settingsService.subscribe((settings) => {
|
||||
this.electronService.ipcRenderer.send('settings.changed', settings);
|
||||
});
|
||||
}
|
||||
|
||||
this.applyTheme(this.themeService.savedTheme + '-theme');
|
||||
this.themeService.themeChanged.subscribe((event: string) => {
|
||||
this.applyTheme(event);
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { AngularReactBrowserModule } from '@angular-react/core';
|
||||
import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||
import { OverlayModule } from '@angular/cdk/overlay';
|
||||
import { CdkTableModule } from '@angular/cdk/table';
|
||||
@ -9,7 +8,6 @@ import { MatSidenavModule } from '@angular/material/sidenav';
|
||||
import { BrowserModule, Title } from '@angular/platform-browser';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { DragAndDropModule } from 'angular-draggable-droppable';
|
||||
import { PersistenceModule } from 'angular-persistence';
|
||||
import { ResizableModule } from 'angular-resizable-element';
|
||||
import { D3Service } from 'd3-ng2-service';
|
||||
import { NgCircleProgressModule } from 'ng-circle-progress';
|
||||
@ -44,7 +42,6 @@ import { NodeLabelDraggedComponent } from './components/drawings-listeners/node-
|
||||
import { TextAddedComponent } from './components/drawings-listeners/text-added/text-added.component';
|
||||
import { TextEditedComponent } from './components/drawings-listeners/text-edited/text-edited.component';
|
||||
import { HelpComponent } from './components/help/help.component';
|
||||
import { ReportIssueComponent } from './components/help/report-issue/report-issue.component';
|
||||
import { InstallSoftwareComponent } from './components/installed-software/install-software/install-software.component';
|
||||
import { InstalledSoftwareComponent } from './components/installed-software/installed-software.component';
|
||||
import { PageNotFoundComponent } from './components/page-not-found/page-not-found.component';
|
||||
@ -204,6 +201,7 @@ import { TemplateComponent } from './components/template/template.component';
|
||||
import { TopologySummaryComponent } from './components/topology-summary/topology-summary.component';
|
||||
import { WebConsoleFullWindowComponent } from './components/web-console-full-window/web-console-full-window.component';
|
||||
import { DataSourceFilter } from './filters/dataSourceFilter';
|
||||
import { AuthImageFilter } from './filters/authImageFilter';
|
||||
import { DateFilter } from './filters/dateFilter.pipe';
|
||||
import { NameFilter } from './filters/nameFilter.pipe';
|
||||
import { ProjectsFilter } from './filters/projectsFilter.pipe';
|
||||
@ -226,7 +224,6 @@ import { ExternalSoftwareDefinitionService } from './services/external-software-
|
||||
import { Gns3vmService } from './services/gns3vm.service';
|
||||
import { GoogleAnalyticsService } from './services/google-analytics.service';
|
||||
import { HttpServer, ServerErrorHandler } from './services/http-server.service';
|
||||
import { IndexedDbService } from './services/indexed-db.service';
|
||||
import { InfoService } from './services/info.service';
|
||||
import { InstalledSoftwareService } from './services/installed-software.service';
|
||||
import { IosConfigurationService } from './services/ios-configuration.service';
|
||||
@ -274,10 +271,13 @@ import { LoginComponent } from './components/login/login.component';
|
||||
import { LoginService } from './services/login.service';
|
||||
import { HttpRequestsInterceptor } from './interceptors/http.interceptor';
|
||||
import { UserManagementComponent } from './components/user-management/user-management.component'
|
||||
import { UserService } from './services/user.service';
|
||||
import { LoggedUserComponent } from './components/users/logged-user/logged-user.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
LoggedUserComponent,
|
||||
ProjectMapComponent,
|
||||
LoginComponent,
|
||||
ServersComponent,
|
||||
@ -382,6 +382,7 @@ import { UserManagementComponent } from './components/user-management/user-manag
|
||||
DataSourceFilter,
|
||||
TemplateFilter,
|
||||
ProjectsFilter,
|
||||
AuthImageFilter,
|
||||
ListOfSnapshotsComponent,
|
||||
CustomAdaptersComponent,
|
||||
NodesMenuComponent,
|
||||
@ -458,11 +459,10 @@ import { UserManagementComponent } from './components/user-management/user-manag
|
||||
TemplateNameDialogComponent,
|
||||
ConfigureCustomAdaptersDialogComponent,
|
||||
EditNetworkConfigurationDialogComponent,
|
||||
ReportIssueComponent,
|
||||
UserManagementComponent
|
||||
UserManagementComponent,
|
||||
ProjectReadmeComponent
|
||||
],
|
||||
imports: [
|
||||
AngularReactBrowserModule,
|
||||
BrowserModule,
|
||||
HttpClientModule,
|
||||
AppRoutingModule,
|
||||
@ -471,7 +471,6 @@ import { UserManagementComponent } from './components/user-management/user-manag
|
||||
BrowserAnimationsModule,
|
||||
CdkTableModule,
|
||||
CartographyModule,
|
||||
PersistenceModule,
|
||||
NgxElectronModule,
|
||||
FileUploadModule,
|
||||
MatSidenavModule,
|
||||
@ -496,7 +495,6 @@ import { UserManagementComponent } from './components/user-management/user-manag
|
||||
NodeService,
|
||||
LinkService,
|
||||
DrawingService,
|
||||
IndexedDbService,
|
||||
HttpServer,
|
||||
SnapshotService,
|
||||
ProgressDialogService,
|
||||
@ -558,7 +556,8 @@ import { UserManagementComponent } from './components/user-management/user-manag
|
||||
Title,
|
||||
ApplianceService,
|
||||
UpdatesService,
|
||||
LoginService
|
||||
LoginService,
|
||||
UserService
|
||||
],
|
||||
entryComponents: [
|
||||
AddServerDialogComponent,
|
||||
|
@ -26,9 +26,7 @@ export class DrawingComponent implements OnInit {
|
||||
ngOnInit() {
|
||||
try {
|
||||
this.drawing.element = this.svgToDrawingConverter.convert(this.drawing.svg);
|
||||
} catch (error) {
|
||||
console.log(`Cannot convert due to Error: '${error}'`);
|
||||
}
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
OnDragging(evt) {
|
||||
|
@ -44,9 +44,7 @@ export class DrawingsWidget implements Widget {
|
||||
layer.drawings.forEach((d: MapDrawing) => {
|
||||
try {
|
||||
d.element = this.svgToDrawingConverter.convert(d.svg);
|
||||
} catch (error) {
|
||||
console.log(`Cannot convert due to Error: '${error}'`);
|
||||
}
|
||||
} catch (error) {}
|
||||
});
|
||||
return layer.drawings;
|
||||
},
|
||||
@ -81,9 +79,7 @@ export class DrawingsWidget implements Widget {
|
||||
.on('start', (datum: MapDrawing) => {
|
||||
document.body.style.cursor = 'ns-resize';
|
||||
topEdge = datum.y;
|
||||
console.log('started');
|
||||
y = event.sourceEvent.clientY - this.context.getZeroZeroTransformationPoint().y;
|
||||
// startEvent = event;
|
||||
})
|
||||
.on('drag', (datum: MapDrawing) => {
|
||||
const evt = event;
|
||||
@ -91,55 +87,10 @@ export class DrawingsWidget implements Widget {
|
||||
y = event.sourceEvent.clientY - this.context.getZeroZeroTransformationPoint().y;
|
||||
let height = datum.element.height - dy;
|
||||
if (height < 0) {
|
||||
// height = datum.y - startEvent.y;
|
||||
datum.y += height;
|
||||
height = topEdge - datum.y;
|
||||
// console.log(topEdge - datum.y);
|
||||
}
|
||||
console.log('Height', height);
|
||||
datum.element.height = height;
|
||||
|
||||
// datum.element.height -= dy;
|
||||
// if(datum.element.height < 0) {
|
||||
// datum.y -= datum.element.height;
|
||||
// datum.element.height = Math.abs(datum.element.height);
|
||||
// }
|
||||
|
||||
// if (!isReflectedVertical) {
|
||||
// if ((datum.element.height + evt.dy) < 0) {
|
||||
// isReflectedVertical = true;
|
||||
// y = topEdge;
|
||||
// console.log(y);
|
||||
// datum.element.height = Math.abs(datum.element.height + evt.dy);
|
||||
// console.log(datum.element.height);
|
||||
// } else {
|
||||
// datum.element.height += evt.dy;
|
||||
|
||||
// if (datum.element instanceof EllipseElement){
|
||||
// (datum.element as EllipseElement).cy = (datum.element as EllipseElement).cy + evt.dy/2 < 0 ? 1 : (datum.element as EllipseElement).cy += evt.dy/2;
|
||||
// (datum.element as EllipseElement).ry = (datum.element as EllipseElement).ry + evt.dy/2 < 0 ? 1 : (datum.element as EllipseElement).ry += evt.dy/2;
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// dy = y - (evt.sourceEvent.clientY - this.context.getZeroZeroTransformationPoint().y);
|
||||
// y = evt.sourceEvent.clientY - this.context.getZeroZeroTransformationPoint().y;
|
||||
|
||||
// if ((datum.element.height + dy) < 0){
|
||||
// isReflectedVertical = false;
|
||||
// y = topEdge;
|
||||
// console.log(y);
|
||||
// datum.element.height = Math.abs(datum.element.height + evt.dy);
|
||||
// console.log(datum.element.height);
|
||||
// } else {
|
||||
// datum.y = evt.sourceEvent.clientY - this.context.getZeroZeroTransformationPoint().y;
|
||||
// datum.element.height += dy;
|
||||
// if (datum.element instanceof EllipseElement) {
|
||||
// (datum.element as EllipseElement).cy = (datum.element as EllipseElement).cy + dy/2 < 0 ? 1 : (datum.element as EllipseElement).cy += dy/2;
|
||||
// (datum.element as EllipseElement).ry = (datum.element as EllipseElement).ry + dy/2 < 0 ? 1 : (datum.element as EllipseElement).ry += dy/2;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
this.redrawDrawing(view, datum);
|
||||
})
|
||||
.on('end', (datum: MapDrawing) => {
|
||||
|
@ -46,9 +46,7 @@ export class DrawingsWidget implements Widget {
|
||||
layer.drawings.forEach((d: MapDrawing) => {
|
||||
try {
|
||||
d.element = this.svgToDrawingConverter.convert(d.svg);
|
||||
} catch (error) {
|
||||
console.log(`Cannot convert due to Error: '${error}'`);
|
||||
}
|
||||
} catch (error) {}
|
||||
});
|
||||
return layer.drawings;
|
||||
},
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { EventEmitter, Injectable } from '@angular/core';
|
||||
import { SymbolService } from '../../services/symbol.service';
|
||||
import { event, select } from 'd3-selection';
|
||||
import { MapSettingsService } from '../../services/mapsettings.service';
|
||||
import { ClickedDataEvent } from '../events/event-source';
|
||||
@ -22,7 +23,8 @@ export class NodeWidget implements Widget {
|
||||
private selectionManager: SelectionManager,
|
||||
private labelWidget: LabelWidget,
|
||||
private nodesEventSource: NodesEventSource,
|
||||
private mapSettingsService: MapSettingsService
|
||||
private mapSettingsService: MapSettingsService,
|
||||
private symbolService: SymbolService
|
||||
) {}
|
||||
|
||||
public draw(view: SVGSelection) {
|
||||
@ -88,7 +90,13 @@ export class NodeWidget implements Widget {
|
||||
event.preventDefault();
|
||||
self.onContextConsoleMenu.emit(new NodeContextMenu(event, n));
|
||||
})
|
||||
.attr('xnode:href', (n: MapNode) => n.symbolUrl)
|
||||
.attr('xnode:href', (n: MapNode) => {
|
||||
let symbol = this.symbolService.get(n.symbol.split('/')[2]);
|
||||
if (symbol) {
|
||||
return 'data:image/svg+xml;base64,' + btoa(symbol.raw);
|
||||
}
|
||||
return '';
|
||||
})
|
||||
.attr('width', (n: MapNode) => {
|
||||
if (!n.width) return 60;
|
||||
return n.width;
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { Injector } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { SettingsService } from '../../services/settings.service';
|
||||
import { MockedSettingsService } from '../../services/settings.service.spec';
|
||||
import { ToasterService } from '../../services/toaster.service';
|
||||
import { MockedToasterService } from '../../services/toaster.service.spec';
|
||||
import { SentryErrorHandler } from './sentry-error-handler';
|
||||
@ -17,19 +16,21 @@ class MockedToasterErrorHandler extends ToasterErrorHandler {
|
||||
describe('ToasterErrorHandler', () => {
|
||||
let handler: ToasterErrorHandler;
|
||||
let toasterService: MockedToasterService;
|
||||
let settingsService: SettingsService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: ToasterService, useClass: MockedToasterService },
|
||||
{ provide: SettingsService, useClass: MockedSettingsService },
|
||||
{ provide: SettingsService},
|
||||
SentryErrorHandler,
|
||||
ToasterErrorHandler,
|
||||
],
|
||||
});
|
||||
|
||||
handler = new MockedToasterErrorHandler(TestBed.get(Injector));
|
||||
handler = new MockedToasterErrorHandler(TestBed.inject(Injector));
|
||||
toasterService = TestBed.get(ToasterService);
|
||||
settingsService = TestBed.inject(SettingsService);
|
||||
});
|
||||
|
||||
it('should call toaster with error message', () => {
|
||||
|
@ -21,7 +21,15 @@ export class BundledServerFinderComponent implements OnInit {
|
||||
ngOnInit() {
|
||||
this.progressService.activate();
|
||||
setTimeout(() => {
|
||||
let port = parseInt(this.document.location.port, 10) ? parseInt(this.document.location.port, 10) : 80;
|
||||
let port;
|
||||
|
||||
if (parseInt(this.document.location.port, 10)) {
|
||||
port = parseInt(this.document.location.port, 10);
|
||||
} else if (this.document.location.protocol == "https:") {
|
||||
port = 443;
|
||||
} else {
|
||||
port = 80;
|
||||
}
|
||||
|
||||
this.serverService.getLocalServer(this.document.location.hostname, port).then((server: Server) => {
|
||||
this.progressService.deactivate();
|
||||
|
@ -59,7 +59,6 @@ export class DirectLinkComponent implements OnInit {
|
||||
const servers = await this.serverService.findAll();
|
||||
const server = servers.filter((server) => server.host === this.serverIp && server.port === this.serverPort)[0];
|
||||
|
||||
console.log(servers);
|
||||
if (server) {
|
||||
this.router.navigate(['/server', server.id, 'project', this.projectId]);
|
||||
} else {
|
||||
|
@ -31,9 +31,6 @@
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
</div>
|
||||
<button mat-button class="full-width">
|
||||
<a href="https://docs.gns3.com/docs/"> Go to documentation </a>
|
||||
</button>
|
||||
<button mat-button routerLink="/help/reportissue" class="full-width">Report an issue</button>
|
||||
<button mat-button color="primary" class="full-width" (click)="goToDocumentation()">Go to documentation</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,4 +1,4 @@
|
||||
.full-width {
|
||||
width: 50%;
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
@ -28,4 +28,8 @@ export class HelpComponent implements OnInit {
|
||||
this.releasenotes = data.replace(new RegExp('\n', 'g'), '<br />');
|
||||
});
|
||||
}
|
||||
|
||||
goToDocumentation() {
|
||||
window.location.href = "https://docs.gns3.com/docs/";
|
||||
}
|
||||
}
|
||||
|
@ -1,24 +0,0 @@
|
||||
import React, {Component} from "react";
|
||||
import Form from 'react-bootstrap/Form';
|
||||
|
||||
const formGroupStyle = {
|
||||
margin: '20px'
|
||||
};
|
||||
|
||||
class FilterComponent extends Component<any, any> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Form.Group style={formGroupStyle}>
|
||||
<Form.Control size="lg" type="text" placeholder="Search by keyword" value={this.props.filter} onChange={this.props.handleChange} />
|
||||
</Form.Group>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default FilterComponent;
|
@ -1,95 +0,0 @@
|
||||
import React, { Component } from "react";
|
||||
import Card from 'react-bootstrap/Card';
|
||||
import Spinner from 'react-bootstrap/Spinner';
|
||||
import IssueComponent from './issue';
|
||||
import FilterComponent from './filter';
|
||||
import e from '../../../event-bus';
|
||||
|
||||
const wrapperStyle = {
|
||||
justifyContent: 'center' as 'center',
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
flexDirection: 'row' as 'row',
|
||||
flexWrap: 'wrap' as 'wrap',
|
||||
margin: '20px'
|
||||
};
|
||||
|
||||
const cardStyle = {
|
||||
width: '300px',
|
||||
margin: '20px'
|
||||
};
|
||||
|
||||
const cardTitleStyle = {
|
||||
color: 'red'
|
||||
};
|
||||
const message = 'Thank you for reporting the issue!';
|
||||
const apiUrl = 'https://api.github.com/repos/GNS3/gns3-web-ui/issues';
|
||||
const newIssueLink = 'https://github.com/GNS3/gns3-web-ui/issues/new';
|
||||
|
||||
class IssueListComponent extends Component<any, any> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
issues: [],
|
||||
filteredIssues: [],
|
||||
isFetching: true
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
fetch(apiUrl)
|
||||
.then(response => response.json())
|
||||
.then(data => this.setState({
|
||||
issues: data,
|
||||
filteredIssues: data,
|
||||
isFetching: false
|
||||
}));
|
||||
}
|
||||
|
||||
handleChange = (event) => {
|
||||
let filter = event.target.value;
|
||||
let filteredIssues = this.state.issues;
|
||||
|
||||
filteredIssues = filteredIssues.filter((issue) => {
|
||||
return issue.title.toLowerCase().includes(filter.toLowerCase())
|
||||
});
|
||||
|
||||
this.setState({
|
||||
filteredIssues: filteredIssues
|
||||
});
|
||||
}
|
||||
|
||||
newIssueOpened = (event) => {
|
||||
e.emit('message', { text: message });
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<FilterComponent handleChange={this.handleChange} filter={this.state.filter} />
|
||||
</div>
|
||||
|
||||
{this.state.isFetching ? (
|
||||
<div style={wrapperStyle}>
|
||||
<Spinner animation="grow" />
|
||||
</div>
|
||||
) : (
|
||||
<div style={wrapperStyle}>
|
||||
{this.state.filteredIssues.map(issue =>
|
||||
<IssueComponent key={issue.html_url} data={issue}/>
|
||||
)}
|
||||
<Card style={cardStyle}>
|
||||
<Card.Body>
|
||||
<Card.Title style={cardTitleStyle}>Don't see your issue here?</Card.Title>
|
||||
<Card.Link onClick={this.newIssueOpened} href={newIssueLink} target = "_blank">Open new issue</Card.Link>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default IssueListComponent;
|
@ -1,42 +0,0 @@
|
||||
import React, { Component } from "react";
|
||||
import Card from 'react-bootstrap/Card';
|
||||
|
||||
const cardStyle = {
|
||||
width: '300px',
|
||||
margin: '20px'
|
||||
};
|
||||
|
||||
const cardTitleStyle = {
|
||||
color: 'black'
|
||||
};
|
||||
|
||||
const cardTextStyle = {
|
||||
color: 'black',
|
||||
overflow: 'auto',
|
||||
height: '100px'
|
||||
};
|
||||
|
||||
class IssueComponent extends Component<any, any> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
data: this.props.data
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const issue = this.state.data;
|
||||
return (
|
||||
<Card key={issue.html_url} style={cardStyle}>
|
||||
<Card.Body>
|
||||
<Card.Title style={cardTitleStyle}>{issue.title}</Card.Title>
|
||||
<Card.Subtitle className="mb-2 text-muted">Status: {issue.state}</Card.Subtitle>
|
||||
<Card.Text style={cardTextStyle}>{issue.body}</Card.Text>
|
||||
<Card.Link href={issue.html_url} target = "_blank">{issue.html_url}</Card.Link>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default IssueComponent;
|
@ -1 +0,0 @@
|
||||
<span #issueListComponentContainer></span>
|
@ -1,52 +0,0 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import {
|
||||
AfterViewInit,
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnInit,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
Output,
|
||||
SimpleChanges,
|
||||
ViewChild,
|
||||
ViewEncapsulation,
|
||||
AfterContentInit
|
||||
} from '@angular/core';
|
||||
import IssueListComponent from '../report-issue/issue-list';
|
||||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import e from '../../../event-bus';
|
||||
import { ToasterService } from '../../../services/toaster.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-report-issue',
|
||||
templateUrl: './report-issue.component.html',
|
||||
styleUrls: ['./report-issue.component.scss']
|
||||
})
|
||||
export class ReportIssueComponent implements OnDestroy, AfterViewInit, AfterContentInit {
|
||||
@ViewChild('issueListComponentContainer') containerRef: ElementRef;
|
||||
|
||||
constructor(private toasterService: ToasterService) {}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.render();
|
||||
}
|
||||
|
||||
ngAfterContentInit() {
|
||||
e.on('message', message => {
|
||||
this.toasterService.success(message.text);
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
ReactDOM.unmountComponentAtNode(this.containerRef.nativeElement);
|
||||
}
|
||||
|
||||
private render() {
|
||||
ReactDOM.render(<div>
|
||||
<IssueListComponent />
|
||||
</div>, this.containerRef.nativeElement);
|
||||
}
|
||||
}
|
@ -18,7 +18,7 @@
|
||||
<mat-error *ngIf="loginForm.get('username').hasError('required')">You must enter username</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<input matInput formControlName="password" placeholder="Password" />
|
||||
<input matInput type="password" formControlName="password" placeholder="Password" />
|
||||
<mat-error *ngIf="loginForm.get('password').hasError('required')">You must enter password</mat-error>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
|
@ -18,9 +18,7 @@ export class CustomAdaptersComponent {
|
||||
public adapters: CustomAdapter[];
|
||||
public numberOfAdapters: number;
|
||||
|
||||
constructor() {
|
||||
console.log(this.networkTypes);
|
||||
}
|
||||
constructor() {}
|
||||
|
||||
cancelConfigureCustomAdapters() {
|
||||
this.closeConfiguratorEmitter.emit(false);
|
||||
@ -28,8 +26,6 @@ export class CustomAdaptersComponent {
|
||||
|
||||
configureCustomAdapters() {
|
||||
this.adapters = [];
|
||||
console.log(this.customAdapters);
|
||||
|
||||
this.customAdapters.adapters.forEach((n) => {
|
||||
this.adapters.push({
|
||||
adapter_number: n.adapter_number,
|
||||
|
@ -32,7 +32,7 @@
|
||||
lazyimg
|
||||
[ngClass]="{ imageSelected: isSelected === symbol.symbol_id }"
|
||||
class="image"
|
||||
[src]="getImageSourceForTemplate(symbol.symbol_id)"
|
||||
[src]="getImageSourceForTemplate(symbol.symbol_id)| authImage: server | async"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
import { Server } from '../../../../models/server';
|
||||
import { Symbol } from '../../../../models/symbol';
|
||||
import { SymbolService } from '../../../../services/symbol.service';
|
||||
@ -18,7 +19,7 @@ export class SymbolsComponent implements OnInit {
|
||||
isSelected: string = '';
|
||||
searchText: string = '';
|
||||
|
||||
constructor(private symbolService: SymbolService) {}
|
||||
constructor(private symbolService: SymbolService, private domSanitizer: DomSanitizer) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.isSelected = this.symbol;
|
||||
@ -76,6 +77,11 @@ export class SymbolsComponent implements OnInit {
|
||||
width=\"${imageToUpload.width}\">\n<image height=\"${imageToUpload.height}\" width=\"${imageToUpload.width}\" xlink:href=\"${image}\"/>\n</svg>`;
|
||||
}
|
||||
|
||||
getImage(symbolFilename: string) {
|
||||
let symbol = this.symbolService.get(symbolFilename);
|
||||
return this.domSanitizer.bypassSecurityTrustUrl(`data:image/svg+xml;base64,${btoa(symbol.raw)}`);
|
||||
}
|
||||
|
||||
getImageSourceForTemplate(symbol: string) {
|
||||
return `${this.server.protocol}//${this.server.host}:${this.server.port}/v3/symbols/${symbol}/raw`;
|
||||
}
|
||||
|
@ -1,21 +1,47 @@
|
||||
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { ToasterService } from '../../../services/toaster.service';
|
||||
import { MapSettingsService } from '../../../services/mapsettings.service';
|
||||
import { NodeConsoleService } from '../../../services/nodeConsole.service';
|
||||
import { ThemeService } from '../../../services/theme.service';
|
||||
import { ConsoleWrapperComponent } from './console-wrapper.component';
|
||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
|
||||
describe('ConsoleWrapperComponent', () => {
|
||||
let fixture: ComponentFixture<ConsoleWrapperComponent>;
|
||||
let component: ConsoleWrapperComponent;
|
||||
|
||||
let nodeConsoleService: NodeConsoleService;
|
||||
let themeService: ThemeService;
|
||||
let mapSettingsService: MapSettingsService;
|
||||
let toasterService: ToasterService;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [RouterTestingModule, MatSnackBarModule],
|
||||
providers: [NodeConsoleService, ThemeService, MapSettingsService, ToasterService]
|
||||
}).compileComponents();
|
||||
|
||||
toasterService = TestBed.inject(ToasterService);
|
||||
nodeConsoleService = TestBed.inject(NodeConsoleService);
|
||||
themeService = TestBed.inject(ThemeService);
|
||||
mapSettingsService = TestBed.inject(MapSettingsService);
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ConsoleWrapperComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should get actual theme', () => {
|
||||
const consoleService = new NodeConsoleService();
|
||||
|
||||
const themeService = autoSpy(ThemeService);
|
||||
themeService.getActualTheme.and.returnValue('light');
|
||||
|
||||
const mapSettingsService = autoSpy(MapSettingsService);
|
||||
const component = new ConsoleWrapperComponent(consoleService, themeService, mapSettingsService);
|
||||
|
||||
component.ngOnInit();
|
||||
|
||||
expect(component.isLightThemeEnabled).toBe(true);
|
||||
expect(component.isLightThemeEnabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { ChangeDetectorRef, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { Router } from '@angular/router';
|
||||
import { ElectronService } from 'ngx-electron';
|
||||
@ -20,7 +21,9 @@ import { ContextConsoleMenuComponent } from './context-console-menu.component';
|
||||
describe('ContextConsoleMenuComponent', () => {
|
||||
let component: ContextConsoleMenuComponent;
|
||||
let fixture: ComponentFixture<ContextConsoleMenuComponent>;
|
||||
let toasterService: MockedToasterService = new MockedToasterService();
|
||||
let nodeConsoleService: NodeConsoleService;
|
||||
let mapSettingsService: MapSettingsService;
|
||||
let toasterService: ToasterService;
|
||||
let router = {
|
||||
url: '',
|
||||
navigate: jasmine.createSpy('navigate'),
|
||||
@ -28,7 +31,6 @@ describe('ContextConsoleMenuComponent', () => {
|
||||
let node = {
|
||||
status: 'started',
|
||||
};
|
||||
let mapSettingsService = new MapSettingsService();
|
||||
|
||||
beforeEach(async(() => {
|
||||
const electronMock = {
|
||||
@ -36,20 +38,25 @@ describe('ContextConsoleMenuComponent', () => {
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [MatMenuModule, BrowserModule],
|
||||
imports: [MatMenuModule, BrowserModule, MatSnackBarModule],
|
||||
providers: [
|
||||
{ provide: ChangeDetectorRef },
|
||||
{ provide: ProjectService, useClass: MockedProjectService },
|
||||
{ provide: ElectronService, useValue: electronMock },
|
||||
{ provide: MapSettingsService, useValue: mapSettingsService },
|
||||
{ provide: NodeConsoleService },
|
||||
{ provide: ConsoleService },
|
||||
{ provide: ToasterService, useValue: toasterService },
|
||||
{ provide: Router, useValue: router },
|
||||
NodeConsoleService,
|
||||
ToasterService,
|
||||
MapSettingsService
|
||||
],
|
||||
declarations: [ContextConsoleMenuComponent, ConsoleDeviceActionComponent, ConsoleDeviceActionBrowserComponent],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
}).compileComponents();
|
||||
|
||||
toasterService = TestBed.inject(ToasterService);
|
||||
mapSettingsService = TestBed.inject(MapSettingsService);
|
||||
nodeConsoleService = TestBed.inject(NodeConsoleService);
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -7,7 +7,6 @@ import { NodeService } from '../../../../../services/node.service';
|
||||
import { ServerService } from '../../../../../services/server.service';
|
||||
import { MockedServerService } from '../../../../../services/server.service.spec';
|
||||
import { SettingsService } from '../../../../../services/settings.service';
|
||||
import { MockedSettingsService } from '../../../../../services/settings.service.spec';
|
||||
import { ToasterService } from '../../../../../services/toaster.service';
|
||||
import { MockedToasterService } from '../../../../../services/toaster.service.spec';
|
||||
import { MockedNodeService } from '../../../project-map.component.spec';
|
||||
@ -18,7 +17,7 @@ describe('ConsoleDeviceActionComponent', () => {
|
||||
let fixture: ComponentFixture<ConsoleDeviceActionComponent>;
|
||||
let electronService;
|
||||
let server: Server;
|
||||
let mockedSettingsService: MockedSettingsService;
|
||||
let settingsService: SettingsService;
|
||||
let mockedServerService: MockedServerService;
|
||||
let mockedToaster: MockedToasterService;
|
||||
let mockedNodeService: MockedNodeService = new MockedNodeService();
|
||||
@ -35,7 +34,6 @@ describe('ConsoleDeviceActionComponent', () => {
|
||||
},
|
||||
};
|
||||
|
||||
mockedSettingsService = new MockedSettingsService();
|
||||
mockedServerService = new MockedServerService();
|
||||
mockedToaster = new MockedToasterService();
|
||||
|
||||
@ -47,13 +45,15 @@ describe('ConsoleDeviceActionComponent', () => {
|
||||
providers: [
|
||||
{ provide: ElectronService, useValue: electronService },
|
||||
{ provide: ServerService, useValue: mockedServerService },
|
||||
{ provide: SettingsService, useValue: mockedSettingsService },
|
||||
{ provide: SettingsService },
|
||||
{ provide: ToasterService, useValue: mockedToaster },
|
||||
{ provide: NodeService, useValue: mockedNodeService },
|
||||
],
|
||||
imports: [MatIconModule],
|
||||
declarations: [ConsoleDeviceActionComponent],
|
||||
}).compileComponents();
|
||||
|
||||
settingsService = TestBed.inject(SettingsService);
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
@ -85,7 +85,7 @@ describe('ConsoleDeviceActionComponent', () => {
|
||||
component.nodes = nodes;
|
||||
component.server = server;
|
||||
|
||||
mockedSettingsService.set('console_command', 'command');
|
||||
settingsService.setConsoleSettings('command');
|
||||
spyOn(component, 'openConsole');
|
||||
});
|
||||
|
||||
@ -105,7 +105,7 @@ describe('ConsoleDeviceActionComponent', () => {
|
||||
});
|
||||
|
||||
it('should set command when it is not defined', async () => {
|
||||
mockedSettingsService.set('console_command', undefined);
|
||||
settingsService.setConsoleSettings(undefined);
|
||||
await component.console();
|
||||
expect(component.openConsole).toHaveBeenCalled();
|
||||
});
|
||||
|
@ -26,8 +26,8 @@ export class ConsoleDeviceActionComponent implements OnInit {
|
||||
ngOnInit() {}
|
||||
|
||||
async console() {
|
||||
let consoleCommand = this.settingsService.get<string>('console_command')
|
||||
? this.settingsService.get<string>('console_command')
|
||||
let consoleCommand = this.settingsService.getConsoleSettings()
|
||||
? this.settingsService.getConsoleSettings()
|
||||
: this.nodeService.getDefaultCommand();
|
||||
const startedNodes = this.nodes.filter((node) => node.status === 'started');
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
<button mat-menu-item (click)="delete()">
|
||||
<button mat-menu-item (click)="confirmDelete()">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span>Delete</span>
|
||||
</button>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { MatBottomSheetModule } from '@angular/material/bottom-sheet';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
@ -24,7 +25,7 @@ describe('DeleteActionComponent', () => {
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [MatIconModule, MatMenuModule, NoopAnimationsModule],
|
||||
imports: [MatIconModule, MatMenuModule, NoopAnimationsModule, MatBottomSheetModule],
|
||||
providers: [
|
||||
{ provide: NodesDataSource, useClass: NodesDataSource },
|
||||
{ provide: DrawingsDataSource, useClass: DrawingsDataSource },
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { MatBottomSheet } from '@angular/material/bottom-sheet';
|
||||
import { ConfirmationBottomSheetComponent } from 'app/components/projects/confirmation-bottomsheet/confirmation-bottomsheet.component';
|
||||
import { DrawingsDataSource } from '../../../../../cartography/datasources/drawings-datasource';
|
||||
import { LinksDataSource } from '../../../../../cartography/datasources/links-datasource';
|
||||
import { NodesDataSource } from '../../../../../cartography/datasources/nodes-datasource';
|
||||
@ -26,11 +28,23 @@ export class DeleteActionComponent implements OnInit {
|
||||
private linksDataSource: LinksDataSource,
|
||||
private nodeService: NodeService,
|
||||
private drawingService: DrawingService,
|
||||
private linkService: LinkService
|
||||
private linkService: LinkService,
|
||||
private bottomSheet: MatBottomSheet
|
||||
) {}
|
||||
|
||||
ngOnInit() {}
|
||||
|
||||
confirmDelete() {
|
||||
this.bottomSheet.open(ConfirmationBottomSheetComponent);
|
||||
let bottomSheetRef = this.bottomSheet._openedBottomSheetRef;
|
||||
bottomSheetRef.instance.message = 'Do you want to delete all selected objects?';
|
||||
const bottomSheetSubscription = bottomSheetRef.afterDismissed().subscribe((result: boolean) => {
|
||||
if (result) {
|
||||
this.delete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
delete() {
|
||||
this.nodes.forEach((node) => {
|
||||
this.nodesDataSource.remove(node);
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { NodeConsoleService } from '../../../../../services/nodeConsole.service';
|
||||
import { Node } from '../../../../../cartography/models/node';
|
||||
import { Server } from '../../../../../models/server';
|
||||
import { ToasterService } from '../../../../../services/toaster.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-http-console-new-tab-action',
|
||||
@ -12,19 +11,11 @@ export class HttpConsoleNewTabActionComponent implements OnInit {
|
||||
@Input() server: Server;
|
||||
@Input() nodes: Node[];
|
||||
|
||||
constructor(private toasterService: ToasterService, private router: Router) {}
|
||||
constructor(private nodeConsoleService: NodeConsoleService) {}
|
||||
|
||||
ngOnInit() {}
|
||||
|
||||
openConsole() {
|
||||
this.nodes.forEach((n) => {
|
||||
if (n.status === 'started') {
|
||||
let url = this.router.url.split('/');
|
||||
let urlString = `/static/web-ui/${url[1]}/${url[2]}/${url[3]}/${url[4]}/nodes/${n.node_id}`;
|
||||
window.open(urlString);
|
||||
} else {
|
||||
this.toasterService.error('To open console please start the node');
|
||||
}
|
||||
});
|
||||
this.nodeConsoleService.openConsolesForAllNodesInNewTabs(this.nodes);
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,7 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { Node } from '../../../../../cartography/models/node';
|
||||
import { Server } from '../../../../../models/server';
|
||||
import { MapSettingsService } from '../../../../../services/mapsettings.service';
|
||||
import { NodeConsoleService } from '../../../../../services/nodeConsole.service';
|
||||
import { ToasterService } from '../../../../../services/toaster.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-http-console-action',
|
||||
@ -13,22 +11,11 @@ export class HttpConsoleActionComponent implements OnInit {
|
||||
@Input() server: Server;
|
||||
@Input() nodes: Node[];
|
||||
|
||||
constructor(
|
||||
private consoleService: NodeConsoleService,
|
||||
private toasterService: ToasterService,
|
||||
private mapSettingsService: MapSettingsService
|
||||
) {}
|
||||
constructor(private nodeConsoleService: NodeConsoleService) {}
|
||||
|
||||
ngOnInit() {}
|
||||
|
||||
openConsole() {
|
||||
this.nodes.forEach((n) => {
|
||||
if (n.status === 'started') {
|
||||
this.mapSettingsService.logConsoleSubject.next(true);
|
||||
this.consoleService.openConsoleForNode(n);
|
||||
} else {
|
||||
this.toasterService.error('To open console please start the node');
|
||||
}
|
||||
});
|
||||
this.nodeConsoleService.openConsolesForAllNodesInWidget(this.nodes);
|
||||
}
|
||||
}
|
||||
|
@ -8,12 +8,12 @@
|
||||
<app-stop-node-action *ngIf="nodes.length" [server]="server" [nodes]="nodes"></app-stop-node-action>
|
||||
<app-reload-node-action *ngIf="nodes.length" [server]="server" [nodes]="nodes"></app-reload-node-action>
|
||||
<app-http-console-action
|
||||
*ngIf="!projectService.isReadOnly(project) && nodes.length === 1"
|
||||
*ngIf="!projectService.isReadOnly(project) && nodes.length > 0"
|
||||
[server]="server"
|
||||
[nodes]="nodes"
|
||||
></app-http-console-action>
|
||||
<app-http-console-new-tab-action
|
||||
*ngIf="!projectService.isReadOnly(project) && nodes.length === 1"
|
||||
*ngIf="!projectService.isReadOnly(project) && nodes.length > 0"
|
||||
[server]="server"
|
||||
[nodes]="nodes"
|
||||
></app-http-console-new-tab-action>
|
||||
|
@ -4,6 +4,8 @@ import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { ToasterService } from '../../../services/toaster.service';
|
||||
import { of } from 'rxjs';
|
||||
import { NodesDataSource } from '../../../cartography/datasources/nodes-datasource';
|
||||
import { ProjectWebServiceHandler, WebServiceMessage } from '../../../handlers/project-web-service-handler';
|
||||
@ -14,6 +16,8 @@ import { NodeConsoleService } from '../../../services/nodeConsole.service';
|
||||
import { MockedNodesDataSource, MockedNodeService } from '../project-map.component.spec';
|
||||
import { LogConsoleComponent } from './log-console.component';
|
||||
import { LogEventsDataSource } from './log-events-datasource';
|
||||
import { MapSettingsService } from '../../../services/mapsettings.service';
|
||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
|
||||
export class MockedProjectWebServiceHandler {
|
||||
public nodeNotificationEmitter = new EventEmitter<WebServiceMessage>();
|
||||
@ -31,23 +35,32 @@ describe('LogConsoleComponent', () => {
|
||||
let mockedNodeService: MockedNodeService = new MockedNodeService();
|
||||
let mockedNodesDataSource: MockedNodesDataSource = new MockedNodesDataSource();
|
||||
let mockedProjectWebServiceHandler: MockedProjectWebServiceHandler = new MockedProjectWebServiceHandler();
|
||||
let nodeConsoleService: NodeConsoleService;
|
||||
let mapSettingsService: MapSettingsService;
|
||||
let toasterService: ToasterService;
|
||||
|
||||
let httpServer = new HttpServer({} as HttpClient, {} as ServerErrorHandler);
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [HttpClientTestingModule, MatMenuModule, BrowserModule],
|
||||
imports: [HttpClientTestingModule, RouterTestingModule, MatMenuModule, BrowserModule, MatSnackBarModule],
|
||||
providers: [
|
||||
{ provide: ProjectWebServiceHandler, useValue: mockedProjectWebServiceHandler },
|
||||
{ provide: NodeService, useValue: mockedNodeService },
|
||||
{ provide: NodesDataSource, useValue: mockedNodesDataSource },
|
||||
{ provide: LogEventsDataSource, useClass: LogEventsDataSource },
|
||||
{ provide: HttpServer, useValue: httpServer },
|
||||
{ provide: NodeConsoleService },
|
||||
NodeConsoleService,
|
||||
ToasterService,
|
||||
MapSettingsService
|
||||
],
|
||||
declarations: [LogConsoleComponent],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
}).compileComponents();
|
||||
|
||||
toasterService = TestBed.inject(ToasterService);
|
||||
mapSettingsService = TestBed.inject(MapSettingsService);
|
||||
nodeConsoleService = TestBed.inject(NodeConsoleService);
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -259,6 +259,38 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-item-inside" *ngIf="version.images.cdrom_image">
|
||||
<span>
|
||||
{{ version.images.cdrom_image}}
|
||||
</span>
|
||||
|
||||
<div>
|
||||
<span *ngIf="checkImageFromVersion(version.images.cdrom_image)"
|
||||
><mat-icon matTooltip="Ready to install" matTooltipClass="custom-tooltip">check</mat-icon></span
|
||||
>
|
||||
<span *ngIf="!checkImageFromVersion(version.images.cdrom_image)"
|
||||
><mat-icon matTooltip="Missing" matTooltipClass="custom-tooltip">close</mat-icon></span
|
||||
>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
class="non-visible"
|
||||
#file4
|
||||
(change)="importImage($event, version.images.cdrom_image)"
|
||||
ng2FileSelect
|
||||
[uploader]="uploaderImage"
|
||||
/>
|
||||
<button class="button" mat-raised-button (click)="file4.click()">Import</button>
|
||||
<button
|
||||
class="button"
|
||||
mat-raised-button
|
||||
(click)="downloadImageFromVersion(version.images.cdrom_image)"
|
||||
>
|
||||
Download
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
import { MapSettingsService } from '../../../services/mapsettings.service';
|
||||
import { ElectronService } from 'ngx-electron';
|
||||
import { NodesDataSource } from '../../../cartography/datasources/nodes-datasource';
|
||||
import { Project } from '../../../models/project';
|
||||
@ -7,6 +8,7 @@ import { NodeService } from '../../../services/node.service';
|
||||
import { ServerService } from '../../../services/server.service';
|
||||
import { SettingsService } from '../../../services/settings.service';
|
||||
import { ToasterService } from '../../../services/toaster.service';
|
||||
import { NodeConsoleService } from '../../../services/nodeConsole.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-nodes-menu',
|
||||
@ -20,17 +22,19 @@ export class NodesMenuComponent {
|
||||
|
||||
constructor(
|
||||
private nodeService: NodeService,
|
||||
private nodeConsoleService: NodeConsoleService,
|
||||
private nodesDataSource: NodesDataSource,
|
||||
private toasterService: ToasterService,
|
||||
private serverService: ServerService,
|
||||
private settingsService: SettingsService,
|
||||
private mapSettingsService: MapSettingsService,
|
||||
private electronService: ElectronService
|
||||
) {}
|
||||
|
||||
async startConsoleForAllNodes() {
|
||||
if (this.electronService.isElectronApp) {
|
||||
let consoleCommand = this.settingsService.get<string>('console_command')
|
||||
? this.settingsService.get<string>('console_command')
|
||||
let consoleCommand = this.settingsService.getConsoleSettings()
|
||||
? this.settingsService.getConsoleSettings()
|
||||
: this.nodeService.getDefaultCommand();
|
||||
|
||||
let nodes = this.nodesDataSource.getItems();
|
||||
@ -48,7 +52,11 @@ export class NodesMenuComponent {
|
||||
await this.electronService.remote.require('./console-executor.js').openConsole(request);
|
||||
}
|
||||
} else {
|
||||
this.toasterService.error('Option to start all nodes not available in web browser.');
|
||||
if (this.mapSettingsService.openConsolesInWidget) {
|
||||
this.nodeConsoleService.openConsolesForAllNodesInWidget(this.nodesDataSource.getItems());
|
||||
} else {
|
||||
this.nodeConsoleService.openConsolesForAllNodesInNewTabs(this.nodesDataSource.getItems());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
<div *ngIf="project" [ngClass]="{ lightTheme: isLightThemeEnabled }" class="project-map">
|
||||
<app-d3-map
|
||||
*ngIf="!settings.angular_map"
|
||||
*ngIf="!settings.angular_map && symbolsLoaded"
|
||||
[server]="server"
|
||||
[project]="project"
|
||||
[symbols]="symbols"
|
||||
@ -64,7 +64,6 @@
|
||||
<div class="menu-button-group">
|
||||
<app-nodes-menu [server]="server" [project]="project"></app-nodes-menu>
|
||||
<app-context-menu [project]="project" [server]="server"></app-context-menu>
|
||||
<app-context-console-menu [project]="project" [server]="server"></app-context-console-menu>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
@ -255,12 +254,6 @@
|
||||
(closeConsole)="toggleShowConsole($event)"
|
||||
></app-console-wrapper>
|
||||
</div>
|
||||
<div [ngClass]="{ visible: !isTopologySummaryVisible }">
|
||||
<app-topology-summary
|
||||
*ngIf="project"
|
||||
[server]="server"
|
||||
[project]="project"
|
||||
(closeTopologySummary)="toggleShowTopologySummary($event)"
|
||||
></app-topology-summary>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #topologySummaryContainer></ng-template>
|
||||
|
@ -59,7 +59,6 @@ import { RecentlyOpenedProjectService } from '../../services/recentlyOpenedProje
|
||||
import { ServerService } from '../../services/server.service';
|
||||
import { MockedServerService } from '../../services/server.service.spec';
|
||||
import { SettingsService } from '../../services/settings.service';
|
||||
import { MockedSettingsService } from '../../services/settings.service.spec';
|
||||
import { ToasterService } from '../../services/toaster.service';
|
||||
import { MockedToasterService } from '../../services/toaster.service.spec';
|
||||
import { ToolsService } from '../../services/tools.service';
|
||||
@ -300,7 +299,7 @@ xdescribe('ProjectMapComponent', () => {
|
||||
{ provide: NodesDataSource, useValue: nodesDataSource },
|
||||
{ provide: LinksDataSource, useValue: linksDataSource },
|
||||
{ provide: DrawingsDataSource, useValue: drawingsDataSource },
|
||||
{ provide: SettingsService, useClass: MockedSettingsService },
|
||||
{ provide: SettingsService },
|
||||
{ provide: ToolsService },
|
||||
{ provide: SelectionManager },
|
||||
{ provide: SelectionTool },
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
|
||||
import { ChangeDetectorRef, Component, ComponentFactoryResolver, ComponentRef, Injector, OnDestroy, OnInit, SimpleChange, ViewChild, ViewContainerRef, ViewEncapsulation } from '@angular/core';
|
||||
import { MatBottomSheet } from '@angular/material/bottom-sheet';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
||||
import * as Mousetrap from 'mousetrap';
|
||||
import { from, Observable, Subscription } from 'rxjs';
|
||||
import { map, mergeMap } from 'rxjs/operators';
|
||||
import { map, mergeMap, takeUntil } from 'rxjs/operators';
|
||||
import { D3MapComponent } from '../../cartography/components/d3-map/d3-map.component';
|
||||
import { MapDrawingToDrawingConverter } from '../../cartography/converters/map/map-drawing-to-drawing-converter';
|
||||
import { MapLabelToLabelConverter } from '../../cartography/converters/map/map-label-to-label-converter';
|
||||
@ -74,7 +74,7 @@ import { ImportProjectDialogComponent } from '../projects/import-project-dialog/
|
||||
import { NavigationDialogComponent } from '../projects/navigation-dialog/navigation-dialog.component';
|
||||
import { SaveProjectDialogComponent } from '../projects/save-project-dialog/save-project-dialog.component';
|
||||
import { NodeAddedEvent } from '../template/template-list-dialog/template-list-dialog.component';
|
||||
import { ContextConsoleMenuComponent } from './context-console-menu/context-console-menu.component';
|
||||
import { TopologySummaryComponent } from '../topology-summary/topology-summary.component';
|
||||
import { ContextMenuComponent } from './context-menu/context-menu.component';
|
||||
import { NodeCreatedLabelStylesFixer } from './helpers/node-created-label-styles-fixer';
|
||||
import { NewTemplateDialogComponent } from './new-template-dialog/new-template-dialog.component';
|
||||
@ -105,6 +105,8 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
|
||||
public gridVisibility: boolean = false;
|
||||
public toolbarVisibility: boolean = true;
|
||||
public symbolScaling: boolean = true;
|
||||
public symbolsLoaded: boolean = false;
|
||||
private instance: ComponentRef<TopologySummaryComponent>;
|
||||
|
||||
tools = {
|
||||
selection: true,
|
||||
@ -121,9 +123,9 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
|
||||
public isLightThemeEnabled: boolean = false;
|
||||
|
||||
@ViewChild(ContextMenuComponent) contextMenu: ContextMenuComponent;
|
||||
@ViewChild(ContextConsoleMenuComponent) consoleContextMenu: ContextConsoleMenuComponent;
|
||||
@ViewChild(D3MapComponent) mapChild: D3MapComponent;
|
||||
@ViewChild(ProjectMapMenuComponent) projectMapMenuComponent: ProjectMapMenuComponent;
|
||||
@ViewChild('topologySummaryContainer', {read: ViewContainerRef}) topologySummaryContainer: ViewContainerRef;
|
||||
|
||||
private projectMapSubscription: Subscription = new Subscription();
|
||||
|
||||
@ -173,12 +175,17 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
|
||||
private title: Title,
|
||||
private nodeConsoleService: NodeConsoleService,
|
||||
private symbolService: SymbolService,
|
||||
private cd: ChangeDetectorRef
|
||||
private cd: ChangeDetectorRef,
|
||||
private cfr: ComponentFactoryResolver,
|
||||
private injector: Injector
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.getSettings();
|
||||
this.progressService.activate();
|
||||
this.symbolService.symbolsLoaded.subscribe(loaded => {
|
||||
this.symbolsLoaded = true;
|
||||
});
|
||||
|
||||
if (this.serverService.isServiceInitialized) {
|
||||
this.getData();
|
||||
@ -208,7 +215,6 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.settings = this.settingsService.getAll();
|
||||
this.symbolScaling = this.mapSettingsService.getSymbolScaling();
|
||||
this.isTopologySummaryVisible = this.mapSettingsService.isTopologySummaryVisible;
|
||||
this.isConsoleVisible = this.mapSettingsService.isLogConsoleVisible;
|
||||
this.mapSettingsService.logConsoleSubject.subscribe((value) => (this.isConsoleVisible = value));
|
||||
this.notificationsVisibility = localStorage.getItem('notificationsVisibility') === 'true' ? true : false;
|
||||
@ -216,6 +222,21 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
|
||||
this.gridVisibility = localStorage.getItem('gridVisibility') === 'true' ? true : false;
|
||||
}
|
||||
|
||||
async lazyLoadTopologySummary() {
|
||||
if (this.isTopologySummaryVisible) {
|
||||
const {TopologySummaryComponent} = await import('../topology-summary/topology-summary.component');
|
||||
const componentFactory = this.cfr.resolveComponentFactory(TopologySummaryComponent);
|
||||
this.instance = this.topologySummaryContainer.createComponent(componentFactory, null, this.injector);
|
||||
this.instance.instance.server = this.server;
|
||||
this.instance.instance.project = this.project;
|
||||
} else if (this.instance) {
|
||||
if (this.instance.instance) {
|
||||
this.instance.instance.ngOnDestroy();
|
||||
this.instance.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addSubscriptions() {
|
||||
this.projectMapSubscription.add(
|
||||
this.mapSettingsService.mapRenderedEmitter.subscribe((value: boolean) => {
|
||||
@ -299,8 +320,11 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
|
||||
.pipe(
|
||||
mergeMap((server: Server) => {
|
||||
if (!server) this.router.navigate(['/servers']);
|
||||
|
||||
this.server = server;
|
||||
|
||||
// load symbols
|
||||
this.symbolService.load(this.server);
|
||||
|
||||
return this.projectService.get(server, paramMap.get('project_id')).pipe(
|
||||
map((project) => {
|
||||
return project;
|
||||
@ -314,6 +338,7 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
|
||||
this.projectService.open(this.server, this.project.project_id);
|
||||
this.title.setTitle(this.project.name);
|
||||
this.isInterfaceLabelVisible = this.mapSettingsService.showInterfaceLabels;
|
||||
this.toggleShowTopologySummary(this.mapSettingsService.isTopologySummaryVisible);
|
||||
|
||||
this.recentlyOpenedProjectService.setServerId(this.server.id.toString());
|
||||
this.recentlyOpenedProjectService.setProjectId(this.project.project_id);
|
||||
@ -386,16 +411,27 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
|
||||
|
||||
Mousetrap.bind('del', (event: Event) => {
|
||||
event.preventDefault();
|
||||
const selected = this.selectionManager.getSelected();
|
||||
this.deleteItems();
|
||||
});
|
||||
}
|
||||
|
||||
selected
|
||||
.filter((item) => item instanceof MapNode)
|
||||
.forEach((item: MapNode) => {
|
||||
const node = this.mapNodeToNode.convert(item);
|
||||
this.nodeService.delete(this.server, node).subscribe((data) => {
|
||||
this.toasterService.success('Node has been deleted');
|
||||
deleteItems() {
|
||||
this.bottomSheet.open(ConfirmationBottomSheetComponent);
|
||||
let bottomSheetRef = this.bottomSheet._openedBottomSheetRef;
|
||||
bottomSheetRef.instance.message = 'Do you want to delete all selected objects?';
|
||||
const bottomSheetSubscription = bottomSheetRef.afterDismissed().subscribe((result: boolean) => {
|
||||
if (result) {
|
||||
const selected = this.selectionManager.getSelected();
|
||||
|
||||
selected
|
||||
.filter((item) => item instanceof MapNode)
|
||||
.forEach((item: MapNode) => {
|
||||
const node = this.mapNodeToNode.convert(item);
|
||||
this.nodeService.delete(this.server, node).subscribe((data) => {
|
||||
this.toasterService.success('Node has been deleted');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -448,33 +484,33 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
|
||||
|
||||
const onLinkContextMenu = this.linkWidget.onContextMenu.subscribe((eventLink: LinkContextMenu) => {
|
||||
const link = this.mapLinkToLink.convert(eventLink.link);
|
||||
this.contextMenu.openMenuForListOfElements([], [], [], [link], eventLink.event.pageY, eventLink.event.pageX);
|
||||
this.contextMenu.openMenuForListOfElements([], [], [], [link], eventLink.event.screenY - 60, eventLink.event.screenX);
|
||||
});
|
||||
|
||||
const onEthernetLinkContextMenu = this.ethernetLinkWidget.onContextMenu.subscribe((eventLink: LinkContextMenu) => {
|
||||
const link = this.mapLinkToLink.convert(eventLink.link);
|
||||
this.contextMenu.openMenuForListOfElements([], [], [], [link], eventLink.event.pageY, eventLink.event.pageX);
|
||||
this.contextMenu.openMenuForListOfElements([], [], [], [link], eventLink.event.screenY - 60, eventLink.event.screenX);
|
||||
});
|
||||
|
||||
const onSerialLinkContextMenu = this.serialLinkWidget.onContextMenu.subscribe((eventLink: LinkContextMenu) => {
|
||||
const link = this.mapLinkToLink.convert(eventLink.link);
|
||||
this.contextMenu.openMenuForListOfElements([], [], [], [link], eventLink.event.pageY, eventLink.event.pageX);
|
||||
this.contextMenu.openMenuForListOfElements([], [], [], [link], eventLink.event.screenY - 60, eventLink.event.screenX);
|
||||
});
|
||||
|
||||
const onNodeContextMenu = this.nodeWidget.onContextMenu.subscribe((eventNode: NodeContextMenu) => {
|
||||
const node = this.mapNodeToNode.convert(eventNode.node);
|
||||
this.contextMenu.openMenuForNode(node, eventNode.event.pageY, eventNode.event.pageX);
|
||||
this.contextMenu.openMenuForNode(node, eventNode.event.screenY - 60, eventNode.event.screenX);
|
||||
});
|
||||
|
||||
const onDrawingContextMenu = this.drawingsWidget.onContextMenu.subscribe((eventDrawing: DrawingContextMenu) => {
|
||||
const drawing = this.mapDrawingToDrawing.convert(eventDrawing.drawing);
|
||||
this.contextMenu.openMenuForDrawing(drawing, eventDrawing.event.pageY, eventDrawing.event.pageX);
|
||||
this.contextMenu.openMenuForDrawing(drawing, eventDrawing.event.screenY - 60, eventDrawing.event.screenX);
|
||||
});
|
||||
|
||||
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);
|
||||
this.contextMenu.openMenuForLabel(label, node, eventLabel.event.screenY - 60, eventLabel.event.screenX);
|
||||
});
|
||||
|
||||
const onInterfaceLabelContextMenu = this.interfaceLabelWidget.onContextMenu.subscribe(
|
||||
@ -484,8 +520,8 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
|
||||
this.contextMenu.openMenuForInterfaceLabel(
|
||||
linkNode,
|
||||
link,
|
||||
eventInterfaceLabel.event.pageY,
|
||||
eventInterfaceLabel.event.pageX
|
||||
eventInterfaceLabel.event.screenY - 60,
|
||||
eventInterfaceLabel.event.screenX
|
||||
);
|
||||
}
|
||||
);
|
||||
@ -514,11 +550,6 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
|
||||
this.contextMenu.openMenuForListOfElements(drawings, nodes, labels, links, event.pageY, event.pageX);
|
||||
});
|
||||
|
||||
const onContextConsoleMenu = this.nodeWidget.onContextConsoleMenu.subscribe((eventNode: NodeContextMenu) => {
|
||||
const node = this.mapNodeToNode.convert(eventNode.node);
|
||||
this.consoleContextMenu.openMenu(node, eventNode.event.pageY, eventNode.event.pageX);
|
||||
});
|
||||
|
||||
this.projectMapSubscription.add(onLinkContextMenu);
|
||||
this.projectMapSubscription.add(onEthernetLinkContextMenu);
|
||||
this.projectMapSubscription.add(onSerialLinkContextMenu);
|
||||
@ -527,7 +558,6 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
|
||||
this.projectMapSubscription.add(onContextMenu);
|
||||
this.projectMapSubscription.add(onLabelContextMenu);
|
||||
this.projectMapSubscription.add(onInterfaceLabelContextMenu);
|
||||
this.projectMapSubscription.add(onContextConsoleMenu);
|
||||
this.mapChangeDetectorRef.detectChanges();
|
||||
}
|
||||
|
||||
@ -536,6 +566,9 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
nodeAddedEvent.x = nodeAddedEvent.x / this.mapScaleService.getScale();
|
||||
nodeAddedEvent.y = nodeAddedEvent.y / this.mapScaleService.getScale();
|
||||
|
||||
this.progressService.activate();
|
||||
this.nodeService
|
||||
.createFromTemplate(
|
||||
@ -796,6 +829,7 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
|
||||
public toggleShowTopologySummary(visible: boolean) {
|
||||
this.isTopologySummaryVisible = visible;
|
||||
this.mapSettingsService.toggleTopologySummary(this.isTopologySummaryVisible);
|
||||
this.lazyLoadTopologySummary();
|
||||
}
|
||||
|
||||
public toggleNotifications(visible: boolean) {
|
||||
|
@ -1,10 +1,12 @@
|
||||
.import-button {
|
||||
height: 40px;
|
||||
width: 160px;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.add-button {
|
||||
height: 40px;
|
||||
width: 160px;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
@ -13,3 +15,7 @@
|
||||
margin-left: -470px;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
}
|
||||
|
@ -23,7 +23,6 @@ import { MockedProjectService } from '../../services/project.service.spec';
|
||||
import { ServerService } from '../../services/server.service';
|
||||
import { MockedServerService } from '../../services/server.service.spec';
|
||||
import { Settings, SettingsService } from '../../services/settings.service';
|
||||
import { MockedSettingsService } from '../../services/settings.service.spec';
|
||||
import { ToasterService } from '../../services/toaster.service';
|
||||
import { ConfigureGns3VMDialogComponent } from '../servers/configure-gns3vm-dialog/configure-gns3vm-dialog.component';
|
||||
import { ChooseNameDialogComponent } from './choose-name-dialog/choose-name-dialog.component';
|
||||
@ -70,7 +69,7 @@ xdescribe('ProjectsComponent', () => {
|
||||
providers: [
|
||||
{ provide: ServerService, useClass: MockedServerService },
|
||||
{ provide: ProjectService, useValue: mockedProjectService },
|
||||
{ provide: SettingsService, useClass: MockedSettingsService },
|
||||
{ provide: SettingsService},
|
||||
{ provide: ToasterService },
|
||||
{ provide: ElectronService, useValue: electronService },
|
||||
ProgressService,
|
||||
@ -83,10 +82,10 @@ xdescribe('ProjectsComponent', () => {
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
serverService = TestBed.get(ServerService);
|
||||
settingsService = TestBed.get(SettingsService);
|
||||
projectService = TestBed.get(ProjectService);
|
||||
progressService = TestBed.get(ProgressService);
|
||||
serverService = TestBed.inject(ServerService);
|
||||
settingsService = TestBed.inject(SettingsService);
|
||||
projectService = TestBed.inject(ProjectService);
|
||||
progressService = TestBed.inject(ProgressService);
|
||||
|
||||
server = new Server();
|
||||
server.id = 99;
|
||||
|
@ -35,7 +35,7 @@
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div mat-dialog-actions>
|
||||
<button mat-button (click)="onNoClick()" tabindex="-1" color="accent">No Thanks</button>
|
||||
<button mat-button (click)="onNoClick()" tabindex="-1" color="accent">Cancel</button>
|
||||
<button mat-button (click)="onAddClick()" tabindex="2" mat-raised-button color="primary">Add</button>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -13,19 +13,8 @@
|
||||
<mat-checkbox [(ngModel)]="settings.crash_reports">Send anonymous crash reports</mat-checkbox><br/>
|
||||
<mat-checkbox [(ngModel)]="integrateLinksLabelsToLinks">Integrate link labels to links</mat-checkbox><br/>
|
||||
<mat-checkbox [(ngModel)]="openReadme">Automatically open project README files</mat-checkbox>
|
||||
<mat-checkbox [(ngModel)]="openConsolesInWidget">Open consoles in the widget instead of in new tabs after clicking start consoles for all nodes</mat-checkbox>
|
||||
</div>
|
||||
|
||||
<!-- <div>
|
||||
<mat-checkbox [(ngModel)]="settings.experimental_features"
|
||||
>Enable experimental features (WARNING: IT CAN BREAK YOU LABS!)</mat-checkbox
|
||||
>
|
||||
</div> -->
|
||||
|
||||
<!-- <div>
|
||||
<mat-checkbox [(ngModel)]="settings.angular_map"
|
||||
>Enable experimental Angular Map (WARNING: IT CAN BREAK YOU LABS!)</mat-checkbox
|
||||
>
|
||||
</div> -->
|
||||
</mat-expansion-panel>
|
||||
|
||||
<mat-expansion-panel [expanded]="false">
|
||||
|
@ -6,7 +6,6 @@ import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { PersistenceModule } from 'angular-persistence';
|
||||
import { MapSettingsService } from '../../services/mapsettings.service';
|
||||
import { SettingsService } from '../../services/settings.service';
|
||||
import { ConsoleService } from '../../services/settings/console.service';
|
||||
@ -23,7 +22,8 @@ describe('SettingsComponent', () => {
|
||||
let mapSettingsService = {
|
||||
integrateLinkLabelsToLinks: true,
|
||||
toggleIntegrateInterfaceLabels(val: boolean) {},
|
||||
toggleOpenReadme(val: boolean) {}
|
||||
toggleOpenReadme(val: boolean) {},
|
||||
toggleOpenConsolesInWidget(val: boolean) {}
|
||||
};
|
||||
let consoleService;
|
||||
let updatesService = autoSpy(UpdatesService);
|
||||
@ -38,7 +38,6 @@ describe('SettingsComponent', () => {
|
||||
MatExpansionModule,
|
||||
MatCheckboxModule,
|
||||
FormsModule,
|
||||
PersistenceModule,
|
||||
BrowserAnimationsModule,
|
||||
MatIconModule,
|
||||
MatFormFieldModule,
|
||||
@ -76,11 +75,19 @@ describe('SettingsComponent', () => {
|
||||
};
|
||||
const getAll = spyOn(settingsService, 'getAll').and.returnValue(settings);
|
||||
const setAll = spyOn(settingsService, 'setAll');
|
||||
spyOn(mapSettingsService, 'toggleIntegrateInterfaceLabels');
|
||||
spyOn(mapSettingsService, 'toggleOpenConsolesInWidget');
|
||||
|
||||
component.ngOnInit();
|
||||
|
||||
expect(getAll).toHaveBeenCalled();
|
||||
expect(component.settings).toEqual(settings);
|
||||
|
||||
component.settings.crash_reports = false;
|
||||
component.save();
|
||||
|
||||
expect(setAll).toHaveBeenCalledWith(settings);
|
||||
expect(mapSettingsService.toggleIntegrateInterfaceLabels).toHaveBeenCalled();
|
||||
expect(mapSettingsService.toggleOpenConsolesInWidget).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { MapSettingsService } from '../../services/mapsettings.service';
|
||||
import { SettingsService } from '../../services/settings.service';
|
||||
import { Settings, SettingsService } from '../../services/settings.service';
|
||||
import { ConsoleService } from '../../services/settings/console.service';
|
||||
import { ThemeService } from '../../services/theme.service';
|
||||
import { ToasterService } from '../../services/toaster.service';
|
||||
@ -12,10 +12,11 @@ import { UpdatesService } from '../../services/updates.service';
|
||||
styleUrls: ['./settings.component.scss'],
|
||||
})
|
||||
export class SettingsComponent implements OnInit {
|
||||
settings = { ...SettingsService.DEFAULTS };
|
||||
settings: Settings;
|
||||
consoleCommand: string;
|
||||
integrateLinksLabelsToLinks: boolean;
|
||||
openReadme: boolean;
|
||||
openConsolesInWidget: boolean;
|
||||
|
||||
constructor(
|
||||
private settingsService: SettingsService,
|
||||
@ -31,6 +32,7 @@ export class SettingsComponent implements OnInit {
|
||||
this.consoleCommand = this.consoleService.command;
|
||||
this.integrateLinksLabelsToLinks = this.mapSettingsService.integrateLinkLabelsToLinks;
|
||||
this.openReadme = this.mapSettingsService.openReadme;
|
||||
this.openConsolesInWidget = this.mapSettingsService.openConsolesInWidget;
|
||||
}
|
||||
|
||||
save() {
|
||||
@ -39,6 +41,7 @@ export class SettingsComponent implements OnInit {
|
||||
|
||||
this.mapSettingsService.toggleIntegrateInterfaceLabels(this.integrateLinksLabelsToLinks);
|
||||
this.mapSettingsService.toggleOpenReadme(this.openReadme);
|
||||
this.mapSettingsService.toggleOpenConsolesInWidget(this.openConsolesInWidget);
|
||||
}
|
||||
|
||||
setDarkMode(value: boolean) {
|
||||
|
@ -18,6 +18,6 @@
|
||||
</form>
|
||||
</div>
|
||||
<div mat-dialog-actions>
|
||||
<button mat-button (click)="onNoClick()" tabindex="-1" color="accent">No Thanks</button>
|
||||
<button mat-button (click)="onNoClick()" tabindex="-1" color="accent">Cancel</button>
|
||||
<button mat-button (click)="onAddClick()" tabindex="2" mat-raised-button color="primary">Add</button>
|
||||
</div>
|
||||
|
@ -43,25 +43,25 @@
|
||||
<span *ngIf="i % 4 === 0" class="templateRow">
|
||||
<span class="templateIcon">
|
||||
<div mwlDraggable (dragStart)="dragStart($event)" (dragEnd)="dragEnd($event, filteredTemplates[i])">
|
||||
<img class="image" [src]="getImageSourceForTemplate(filteredTemplates[i])" />
|
||||
<img class="image" alt="Image" [src]="getImageSourceForTemplate(filteredTemplates[i])" />
|
||||
</div>
|
||||
<div class="templateText">{{ filteredTemplates[i].name }}</div>
|
||||
</span>
|
||||
<span *ngIf="filteredTemplates[i + 1]" class="templateIcon">
|
||||
<div mwlDraggable (dragStart)="dragStart($event)" (dragEnd)="dragEnd($event, filteredTemplates[i + 1])">
|
||||
<img class="image" [src]="getImageSourceForTemplate(filteredTemplates[i + 1])" />
|
||||
<img class="image" alt="Image" [src]="getImageSourceForTemplate(filteredTemplates[i + 1])" />
|
||||
</div>
|
||||
<div class="templateText">{{ filteredTemplates[i + 1].name }}</div>
|
||||
</span>
|
||||
<span *ngIf="filteredTemplates[i + 2]" class="templateIcon">
|
||||
<div mwlDraggable (dragStart)="dragStart($event)" (dragEnd)="dragEnd($event, filteredTemplates[i + 2])">
|
||||
<img class="image" [src]="getImageSourceForTemplate(filteredTemplates[i + 2])" />
|
||||
<img class="image" alt="Image" [src]="getImageSourceForTemplate(filteredTemplates[i + 2])" />
|
||||
</div>
|
||||
<div class="templateText">{{ filteredTemplates[i + 2].name }}</div>
|
||||
</span>
|
||||
<span *ngIf="filteredTemplates[i + 3]" class="templateIcon">
|
||||
<div mwlDraggable (dragStart)="dragStart($event)" (dragEnd)="dragEnd($event, filteredTemplates[i + 3])">
|
||||
<img class="image" [src]="getImageSourceForTemplate(filteredTemplates[i + 3])" />
|
||||
<img class="image" alt="Image" [src]="getImageSourceForTemplate(filteredTemplates[i + 3])" />
|
||||
</div>
|
||||
<div class="templateText">{{ filteredTemplates[i + 3].name }}</div>
|
||||
</span>
|
||||
|
@ -8,6 +8,7 @@ import { MapScaleService } from '../../services/mapScale.service';
|
||||
import { SymbolService } from '../../services/symbol.service';
|
||||
import { TemplateService } from '../../services/template.service';
|
||||
import { NodeAddedEvent, TemplateListDialogComponent } from './template-list-dialog/template-list-dialog.component';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
|
||||
@Component({
|
||||
selector: 'app-template',
|
||||
@ -49,7 +50,8 @@ export class TemplateComponent implements OnInit, OnDestroy {
|
||||
private dialog: MatDialog,
|
||||
private templateService: TemplateService,
|
||||
private scaleService: MapScaleService,
|
||||
private symbolService: SymbolService
|
||||
private symbolService: SymbolService,
|
||||
private domSanitizer: DomSanitizer
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
@ -127,7 +129,9 @@ export class TemplateComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
getImageSourceForTemplate(template: Template) {
|
||||
return this.symbolService.getSymbolFromTemplate(this.server, template);
|
||||
let symbol = this.symbolService.getSymbolFromTemplate(template);
|
||||
if (symbol) return this.domSanitizer.bypassSecurityTrustUrl(`data:image/svg+xml;base64,${btoa(symbol.raw)}`);
|
||||
return this.domSanitizer.bypassSecurityTrustUrl('data:image/svg+xml;base64,');
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
|
@ -0,0 +1,20 @@
|
||||
<div class="content">
|
||||
<div class="default-header">
|
||||
<div class="row">
|
||||
<h1 class="col">Logged in user info</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="default-content">
|
||||
<mat-card *ngIf="user">
|
||||
<mat-list>
|
||||
<mat-list-item> Username: {{user.username}} </mat-list-item>
|
||||
<mat-list-item> Full name: {{user.full_name}} </mat-list-item>
|
||||
<mat-list-item> Email: {{user.email}} </mat-list-item>
|
||||
</mat-list>
|
||||
|
||||
<div class="buttons-bar">
|
||||
<button mat-raised-button color="primary" class="full_width" (click)="copyToken()">Click to copy access token</button><br />
|
||||
</div>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,3 @@
|
||||
.full_width {
|
||||
width: 100%;
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { ServerService } from '../../../services/server.service';
|
||||
import { UserService } from '../../../services/user.service';
|
||||
import { ToasterService } from '../../../services/toaster.service';
|
||||
import { User } from '../../../models/users/user';
|
||||
import { Server } from '../../../models/server';
|
||||
|
||||
@Component({
|
||||
selector: 'app-logged-user',
|
||||
templateUrl: './logged-user.component.html',
|
||||
styleUrls: ['./logged-user.component.scss'],
|
||||
})
|
||||
export class LoggedUserComponent implements OnInit {
|
||||
public user: User;
|
||||
public server: Server;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private serverService: ServerService,
|
||||
private userService: UserService,
|
||||
private toasterService: ToasterService
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
let serverId = this.route.snapshot.paramMap.get('server_id');
|
||||
this.serverService.get(+serverId).then((server: Server) => {
|
||||
this.server = server;
|
||||
this.userService.getInformationAboutLoggedUser(server).subscribe((response: any) => {
|
||||
this.user = response;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
copyToken() {
|
||||
const selBox = document.createElement('textarea');
|
||||
selBox.style.position = 'fixed';
|
||||
selBox.style.left = '0';
|
||||
selBox.style.top = '0';
|
||||
selBox.style.opacity = '0';
|
||||
selBox.value = this.server.authToken;
|
||||
document.body.appendChild(selBox);
|
||||
selBox.focus();
|
||||
selBox.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(selBox);
|
||||
|
||||
this.toasterService.success('Token copied');
|
||||
}
|
||||
}
|
26
src/app/filters/authImageFilter.ts
Normal file
26
src/app/filters/authImageFilter.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import { Console } from 'console';
|
||||
import { Server } from '../models/server';
|
||||
import { HttpServer } from '../services/http-server.service';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
|
||||
@Pipe({
|
||||
name: 'authImage'
|
||||
})
|
||||
export class AuthImageFilter implements PipeTransform {
|
||||
|
||||
constructor(
|
||||
private httpServer: HttpServer,
|
||||
private domSanitizer: DomSanitizer
|
||||
) {}
|
||||
|
||||
async transform(src: string, server: Server) {
|
||||
let url = src.split('v3')[1];
|
||||
const imageBlob: Blob = await this.httpServer.getBlob(server, url).toPromise();
|
||||
const reader = new FileReader();
|
||||
return new Promise((resolve, reject) => {
|
||||
reader.onloadend = () => resolve(this.domSanitizer.bypassSecurityTrustUrl(reader.result as string));
|
||||
reader.readAsDataURL(imageBlob);
|
||||
});
|
||||
}
|
||||
}
|
@ -35,6 +35,10 @@
|
||||
<mat-icon>help</mat-icon>
|
||||
<span>Help</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="goToUserInfo()">
|
||||
<mat-icon>person</mat-icon>
|
||||
<span>User info</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="logout()">
|
||||
<mat-icon>highlight_off</mat-icon>
|
||||
<span>Logout</span>
|
||||
|
@ -4,6 +4,7 @@ import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { ServerService } from '../../services/server.service';
|
||||
import { ElectronService } from 'ngx-electron';
|
||||
import { Subject } from 'rxjs';
|
||||
import { ProgressComponent } from '../../common/progress/progress.component';
|
||||
@ -13,6 +14,9 @@ import { ServerManagementService, ServerStateEvent } from '../../services/server
|
||||
import { ToasterService } from '../../services/toaster.service';
|
||||
import { MockedToasterService } from '../../services/toaster.service.spec';
|
||||
import { DefaultLayoutComponent } from './default-layout.component';
|
||||
import { IndexedDbService } from '../../services/indexed-db.service';
|
||||
import { HttpServer, ServerErrorHandler } from '../../services/http-server.service';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
|
||||
class ElectronServiceMock {
|
||||
public isElectronApp: boolean;
|
||||
@ -28,6 +32,10 @@ describe('DefaultLayoutComponent', () => {
|
||||
let fixture: ComponentFixture<DefaultLayoutComponent>;
|
||||
let electronServiceMock: ElectronServiceMock;
|
||||
let serverManagementService = new MockedServerManagementService();
|
||||
let serverService: ServerService;
|
||||
let indexedDbService: IndexedDbService;
|
||||
let httpServer: HttpServer;
|
||||
let errorHandler: ServerErrorHandler;
|
||||
|
||||
beforeEach(async(() => {
|
||||
electronServiceMock = new ElectronServiceMock();
|
||||
@ -35,7 +43,7 @@ describe('DefaultLayoutComponent', () => {
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [DefaultLayoutComponent, ProgressComponent],
|
||||
imports: [MatIconModule, MatMenuModule, MatToolbarModule, RouterTestingModule, MatProgressSpinnerModule],
|
||||
imports: [MatIconModule, MatMenuModule, MatToolbarModule, HttpClientModule, RouterTestingModule, MatProgressSpinnerModule],
|
||||
providers: [
|
||||
{
|
||||
provide: ElectronService,
|
||||
@ -53,9 +61,18 @@ describe('DefaultLayoutComponent', () => {
|
||||
provide: RecentlyOpenedProjectService,
|
||||
useClass: RecentlyOpenedProjectService,
|
||||
},
|
||||
{ provide: ServerService },
|
||||
{ provide: IndexedDbService },
|
||||
{ provide: HttpServer },
|
||||
{ provide: ServerErrorHandler },
|
||||
ProgressService,
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
indexedDbService = TestBed.inject(IndexedDbService);
|
||||
errorHandler = TestBed.inject(ServerErrorHandler);
|
||||
httpServer = TestBed.inject(HttpServer);
|
||||
serverService = TestBed.inject(ServerService);
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -37,7 +37,6 @@ export class DefaultLayoutComponent implements OnInit, OnDestroy {
|
||||
private toasterService: ToasterService,
|
||||
private progressService: ProgressService,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
private serverService: ServerService
|
||||
) {}
|
||||
|
||||
@ -69,6 +68,13 @@ export class DefaultLayoutComponent implements OnInit, OnDestroy {
|
||||
this.shouldStopServersOnClosing = this.electronService.isElectronApp;
|
||||
}
|
||||
|
||||
goToUserInfo() {
|
||||
let serverId = this.router.url.split("/server/")[1].split("/")[0];
|
||||
this.serverService.get(+serverId).then((server: Server) => {
|
||||
this.router.navigate(['/server', server.id, 'loggeduser']);
|
||||
});
|
||||
}
|
||||
|
||||
checkIfUserIsLoginPage() {
|
||||
if (this.router.url.includes("login")) {
|
||||
this.isLoginPage = true;
|
||||
|
10
src/app/models/users/user.ts
Normal file
10
src/app/models/users/user.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export interface User {
|
||||
created_at: string;
|
||||
email: string;
|
||||
full_name: string;
|
||||
is_active: boolean;
|
||||
is_superadmin: boolean;
|
||||
updated_at: string;
|
||||
user_id: string;
|
||||
username: string;
|
||||
}
|
@ -15,7 +15,7 @@ export class BuiltInTemplatesConfigurationService {
|
||||
}
|
||||
|
||||
getConsoleTypesForCloudNodes() {
|
||||
return ['telnet', 'none'];
|
||||
return ['telnet', 'vnc', 'spice', 'http', 'https', 'none'];
|
||||
}
|
||||
|
||||
getCategoriesForEthernetHubs() {
|
||||
|
@ -39,6 +39,23 @@ export type TextOptions = {
|
||||
withCredentials?: boolean;
|
||||
};
|
||||
|
||||
export type BlobOptions = {
|
||||
headers?:
|
||||
| HttpHeaders
|
||||
| {
|
||||
[header: string]: string | string[];
|
||||
};
|
||||
observe?: 'body';
|
||||
params?:
|
||||
| HttpParams
|
||||
| {
|
||||
[param: string]: string | string[];
|
||||
};
|
||||
reportProgress?: boolean;
|
||||
responseType: 'blob';
|
||||
withCredentials?: boolean;
|
||||
};
|
||||
|
||||
export type HeadersOptions = {
|
||||
headers?:
|
||||
| HttpHeaders
|
||||
@ -101,6 +118,16 @@ export class HttpServer {
|
||||
.pipe(catchError(this.errorHandler.handleError));
|
||||
}
|
||||
|
||||
getBlob(server: Server, url: string, options?: BlobOptions): Observable<Blob> {
|
||||
options = this.getBlobOptions(options);
|
||||
const intercepted = this.getOptionsForServer<BlobOptions>(server, url, options);
|
||||
this.requestsNotificationEmitter.emit(`GET ${intercepted.url}`);
|
||||
|
||||
return this.http
|
||||
.get(intercepted.url, intercepted.options as BlobOptions)
|
||||
.pipe(catchError(this.errorHandler.handleError));
|
||||
}
|
||||
|
||||
post<T>(server: Server, url: string, body: any | null, options?: JsonOptions): Observable<T> {
|
||||
options = this.getJsonOptions(options);
|
||||
const intercepted = this.getOptionsForServer(server, url, options);
|
||||
@ -173,6 +200,15 @@ export class HttpServer {
|
||||
return options;
|
||||
}
|
||||
|
||||
private getBlobOptions(options: BlobOptions): BlobOptions {
|
||||
if (!options) {
|
||||
return {
|
||||
responseType: 'blob',
|
||||
};
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
private getOptionsForServer<T extends HeadersOptions>(server: Server, url: string, options: T) {
|
||||
if (server.host && server.port) {
|
||||
if (!server.protocol) {
|
||||
|
@ -1,20 +0,0 @@
|
||||
import { inject, TestBed } from '@angular/core/testing';
|
||||
import { IndexedDbService } from './indexed-db.service';
|
||||
|
||||
describe('IndexedDbService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [IndexedDbService],
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([IndexedDbService], (service: IndexedDbService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should get AngularIndexedDB', inject([IndexedDbService], (service: IndexedDbService) => {
|
||||
const indexeddb = service.get();
|
||||
expect(indexeddb.dbWrapper.dbName).toEqual(IndexedDbService.DATABASE);
|
||||
expect(indexeddb.dbWrapper.dbVersion).toEqual(IndexedDbService.VERSION);
|
||||
}));
|
||||
});
|
@ -1,18 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { AngularIndexedDB } from 'angular2-indexeddb';
|
||||
|
||||
@Injectable()
|
||||
export class IndexedDbService {
|
||||
static VERSION = 1;
|
||||
static DATABASE = 'gns3-web-ui';
|
||||
|
||||
private db: AngularIndexedDB;
|
||||
|
||||
constructor() {
|
||||
this.db = new AngularIndexedDB(IndexedDbService.DATABASE, IndexedDbService.VERSION);
|
||||
}
|
||||
|
||||
public get() {
|
||||
return this.db;
|
||||
}
|
||||
}
|
@ -17,12 +17,14 @@ export class MapSettingsService {
|
||||
public openReadme: boolean;
|
||||
public showInterfaceLabels: boolean = true;
|
||||
public integrateLinkLabelsToLinks: boolean = true;
|
||||
public openConsolesInWidget: boolean = false;
|
||||
|
||||
constructor() {
|
||||
this.isLayerNumberVisible = localStorage.getItem('layersVisibility') === 'true' ? true : false;
|
||||
if (localStorage.getItem('integrateLinkLabelsToLinks'))
|
||||
this.integrateLinkLabelsToLinks = localStorage.getItem('integrateLinkLabelsToLinks') === 'true' ? true : false;
|
||||
|
||||
if (localStorage.getItem('openConsolesInWidget'))
|
||||
this.openConsolesInWidget = localStorage.getItem('openConsolesInWidget') === 'true' ? true : false;
|
||||
let isSymbolScalingEnabled = true;
|
||||
if (localStorage.getItem('symbolScaling')) {
|
||||
isSymbolScalingEnabled = localStorage.getItem('symbolScaling') === 'true' ? true : false;
|
||||
@ -89,12 +91,22 @@ export class MapSettingsService {
|
||||
}
|
||||
|
||||
toggleOpenReadme(value: boolean) {
|
||||
this.openReadme = value;
|
||||
localStorage.removeItem('openReadme');
|
||||
if (value) {
|
||||
localStorage.setItem('openReadme', 'true');
|
||||
} else {
|
||||
localStorage.setItem('openReadme', 'false');
|
||||
}
|
||||
this.openReadme = value;
|
||||
localStorage.removeItem('openReadme');
|
||||
if (value) {
|
||||
localStorage.setItem('openReadme', 'true');
|
||||
} else {
|
||||
localStorage.setItem('openReadme', 'false');
|
||||
}
|
||||
}
|
||||
|
||||
toggleOpenConsolesInWidget(value: boolean) {
|
||||
this.openConsolesInWidget = value;
|
||||
localStorage.removeItem('openConsolesInWidget');
|
||||
if (value) {
|
||||
localStorage.setItem('openConsolesInWidget', 'true');
|
||||
} else {
|
||||
localStorage.setItem('openConsolesInWidget', 'false');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,10 @@ import { EventEmitter, Injectable } from '@angular/core';
|
||||
import { Server } from '../models/server';
|
||||
import { Subject } from 'rxjs';
|
||||
import { Node } from '../cartography/models/node';
|
||||
import { Router } from '@angular/router';
|
||||
import { ToasterService } from './toaster.service';
|
||||
import { MapSettingsService } from './mapsettings.service';
|
||||
import { node } from 'prop-types';
|
||||
|
||||
@Injectable()
|
||||
export class NodeConsoleService {
|
||||
@ -19,7 +23,11 @@ export class NodeConsoleService {
|
||||
private lastNumberOfColumns: number;
|
||||
private lastNumberOfRows: number;
|
||||
|
||||
constructor() {}
|
||||
constructor(
|
||||
private router: Router,
|
||||
private toasterService: ToasterService,
|
||||
private mapSettingsService: MapSettingsService
|
||||
) {}
|
||||
|
||||
getNumberOfColumns() {
|
||||
return this.lastNumberOfColumns;
|
||||
@ -65,6 +73,42 @@ export class NodeConsoleService {
|
||||
|
||||
return `${protocol}://${server.host}:${server.port}/v3/projects/${node.project_id}/nodes/${node.node_id}/console/ws`
|
||||
}
|
||||
|
||||
openConsolesForAllNodesInWidget(nodes: Node[]) {
|
||||
let nodesToStart = 'Please start the following nodes if you want to open consoles for them: ';
|
||||
let nodesToStartCounter = 0;
|
||||
nodes.forEach((n) => {
|
||||
if (n.status === 'started') {
|
||||
this.mapSettingsService.logConsoleSubject.next(true);
|
||||
// this timeout is required due to xterm.js implementation
|
||||
setTimeout(() => { this.openConsoleForNode(n); }, 500);
|
||||
} else {
|
||||
nodesToStartCounter++;
|
||||
nodesToStart += n.name + ' '
|
||||
}
|
||||
});
|
||||
if (nodesToStartCounter > 0) {
|
||||
this.toasterService.error(nodesToStart);
|
||||
}
|
||||
}
|
||||
|
||||
openConsolesForAllNodesInNewTabs(nodes: Node[]) {
|
||||
let nodesToStart = 'Please start the following nodes if you want to open consoles for them: ';
|
||||
let nodesToStartCounter = 0;
|
||||
nodes.forEach((n) => {
|
||||
if (n.status === 'started') {
|
||||
let url = this.router.url.split('/');
|
||||
let urlString = `/static/web-ui/${url[1]}/${url[2]}/${url[3]}/${url[4]}/nodes/${n.node_id}`;
|
||||
window.open(urlString);
|
||||
} else {
|
||||
nodesToStartCounter++;
|
||||
nodesToStart += n.name + ' '
|
||||
}
|
||||
});
|
||||
if (nodesToStartCounter > 0) {
|
||||
this.toasterService.error(nodesToStart);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface ConsoleResizedEvent {
|
||||
|
@ -9,7 +9,6 @@ import { HttpServer } from './http-server.service';
|
||||
import { ProjectService } from './project.service';
|
||||
import { RecentlyOpenedProjectService } from './recentlyOpenedProject.service';
|
||||
import { SettingsService } from './settings.service';
|
||||
import { MockedSettingsService } from './settings.service.spec';
|
||||
import { getTestServer } from './testing';
|
||||
|
||||
/**
|
||||
@ -66,7 +65,7 @@ describe('ProjectService', () => {
|
||||
HttpServer,
|
||||
ProjectService,
|
||||
RecentlyOpenedProjectService,
|
||||
{ provide: SettingsService, useClass: MockedSettingsService },
|
||||
{ provide: SettingsService },
|
||||
],
|
||||
});
|
||||
|
||||
|
@ -1,12 +1,4 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { AngularIndexedDB } from 'angular2-indexeddb';
|
||||
import { Server } from '../models/server';
|
||||
import { HttpServer, ServerErrorHandler } from '../services/http-server.service';
|
||||
import { IndexedDbService } from './indexed-db.service';
|
||||
import { ServerService } from './server.service';
|
||||
|
||||
import Spy = jasmine.Spy;
|
||||
|
||||
export class MockedServerService {
|
||||
public servers: Server[] = [];
|
||||
@ -42,138 +34,3 @@ export class MockedServerService {
|
||||
return `${server.host}:${server.port}`;
|
||||
}
|
||||
}
|
||||
|
||||
describe('ServerService', () => {
|
||||
let indexedDbService: IndexedDbService;
|
||||
let db: AngularIndexedDB;
|
||||
let service: ServerService;
|
||||
let openDatabaseSpy: Spy;
|
||||
let httpServer = new HttpServer({} as HttpClient, {} as ServerErrorHandler);
|
||||
|
||||
beforeEach(() => {
|
||||
indexedDbService = new IndexedDbService();
|
||||
|
||||
db = indexedDbService.get();
|
||||
|
||||
openDatabaseSpy = spyOn(db, 'openDatabase').and.returnValue(Promise.resolve(true));
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
ServerService,
|
||||
{ provide: IndexedDbService, useValue: indexedDbService },
|
||||
{ provide: HttpServer, useValue: httpServer },
|
||||
],
|
||||
});
|
||||
|
||||
service = TestBed.get(ServerService);
|
||||
});
|
||||
|
||||
it('should be created and create database', () => {
|
||||
expect(service).toBeTruthy();
|
||||
expect(db.openDatabase).toHaveBeenCalled();
|
||||
expect(openDatabaseSpy.calls.first().args[0]).toEqual(1);
|
||||
|
||||
const evnt = {
|
||||
currentTarget: {
|
||||
result: {
|
||||
createObjectStore: function () {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
spyOn(evnt.currentTarget.result, 'createObjectStore');
|
||||
|
||||
const upgradeCallback = openDatabaseSpy.calls.first().args[1];
|
||||
upgradeCallback(evnt);
|
||||
|
||||
expect(evnt.currentTarget.result.createObjectStore).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('operations on records', () => {
|
||||
let record: any;
|
||||
|
||||
beforeEach(() => {
|
||||
record = new Server();
|
||||
record.name = 'test';
|
||||
});
|
||||
|
||||
it('should create an object', (done) => {
|
||||
const created = new Server();
|
||||
created.id = 22;
|
||||
|
||||
spyOn(db, 'add').and.returnValue(Promise.resolve(created));
|
||||
|
||||
service.create(record).then((result) => {
|
||||
expect(db.add).toHaveBeenCalledWith('servers', record);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should update an object', (done) => {
|
||||
spyOn(db, 'update').and.returnValue(Promise.resolve(record));
|
||||
|
||||
service.update(record).then((result) => {
|
||||
expect(db.update).toHaveBeenCalledWith('servers', record);
|
||||
expect(result).toEqual(record);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should delete an object', (done) => {
|
||||
record.id = 88;
|
||||
|
||||
spyOn(db, 'delete').and.returnValue(Promise.resolve());
|
||||
|
||||
service.delete(record).then(() => {
|
||||
expect(db.delete).toHaveBeenCalledWith('servers', record.id);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should call findAll', (done) => {
|
||||
spyOn(db, 'getAll').and.returnValue(Promise.resolve([]));
|
||||
|
||||
service.findAll().then((result) => {
|
||||
expect(result).toEqual([]);
|
||||
expect(db.getAll).toHaveBeenCalledWith('servers');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should create local server when missing', (done) => {
|
||||
spyOn(db, 'getAll').and.returnValue(Promise.resolve([]));
|
||||
spyOn(service, 'create').and.returnValue(Promise.resolve(new Server()));
|
||||
|
||||
const expectedServer = new Server();
|
||||
expectedServer.name = 'local';
|
||||
expectedServer.host = 'hostname';
|
||||
expectedServer.port = 9999;
|
||||
expectedServer.location = 'bundled';
|
||||
expectedServer.protocol = 'http:';
|
||||
|
||||
service.getLocalServer('hostname', 9999).then(() => {
|
||||
expect(service.create).toHaveBeenCalledWith(expectedServer);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should update local server when found', (done) => {
|
||||
const server = new Server();
|
||||
server.name = 'local';
|
||||
server.host = 'hostname';
|
||||
server.port = 9999;
|
||||
server.location = 'bundled';
|
||||
|
||||
spyOn(db, 'getAll').and.returnValue(Promise.resolve([server]));
|
||||
spyOn(service, 'update').and.returnValue(Promise.resolve(new Server()));
|
||||
|
||||
service.getLocalServer('hostname-2', 11111).then(() => {
|
||||
server.host = 'hostname-2';
|
||||
server.port = 11111;
|
||||
|
||||
expect(service.update).toHaveBeenCalledWith(server);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -2,146 +2,84 @@ import { Injectable } from '@angular/core';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import { Server, ServerProtocol } from '../models/server';
|
||||
import { HttpServer } from './http-server.service';
|
||||
import { IndexedDbService } from './indexed-db.service';
|
||||
|
||||
@Injectable()
|
||||
export class ServerService {
|
||||
private tablename = 'servers';
|
||||
private ready: Promise<any>;
|
||||
private isIncognitoMode: boolean = false;
|
||||
private serverIdsInIncognitoMode: string[] = [];
|
||||
private serverIds: string[] = [];
|
||||
public serviceInitialized: Subject<boolean> = new Subject<boolean>();
|
||||
public isServiceInitialized: boolean;
|
||||
|
||||
constructor(private indexedDbService: IndexedDbService, private httpServer: HttpServer) {
|
||||
this.ready = this.indexedDbService
|
||||
.get()
|
||||
.openDatabase(1, (evt) => {
|
||||
evt.currentTarget.result.createObjectStore(this.tablename, { keyPath: 'id', autoIncrement: true });
|
||||
})
|
||||
.then(() => {
|
||||
this.indexedDbService
|
||||
.get()
|
||||
.getAll(this.tablename)
|
||||
.then(() => {})
|
||||
.catch(() => {
|
||||
this.isIncognitoMode = true;
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.isIncognitoMode = true;
|
||||
})
|
||||
.finally(() => {
|
||||
this.isServiceInitialized = true;
|
||||
this.serviceInitialized.next(true);
|
||||
});
|
||||
constructor(private httpServer: HttpServer) {
|
||||
this.serverIds = this.getServerIds();
|
||||
this.isServiceInitialized = true;
|
||||
this.serviceInitialized.next(this.isServiceInitialized);
|
||||
}
|
||||
|
||||
public tryToCreateDb() {
|
||||
let promise = new Promise((resolve) => {
|
||||
this.indexedDbService
|
||||
.get()
|
||||
.openDatabase(1, (evt) => {
|
||||
evt.currentTarget.result.createObjectStore(this.tablename, { keyPath: 'id', autoIncrement: true });
|
||||
})
|
||||
.then(() => {})
|
||||
.catch(() => {
|
||||
this.isIncognitoMode = true;
|
||||
});
|
||||
getServerIds() : string[]{
|
||||
let str = localStorage.getItem("serverIds");
|
||||
if (str?.length > 0) {
|
||||
return str.split(",");
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
updateServerIds() {
|
||||
localStorage.removeItem("serverIds");
|
||||
localStorage.setItem("serverIds", this.serverIds.toString());
|
||||
}
|
||||
|
||||
public get(id: number): Promise<Server> {
|
||||
let server: Server = JSON.parse(localStorage.getItem(`server-${id}`));
|
||||
let promise = new Promise<Server>((resolve) => {
|
||||
resolve(server);
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
public get(id: number): Promise<Server> {
|
||||
if (this.isIncognitoMode) {
|
||||
let server: Server = JSON.parse(localStorage.getItem(`server-${id}`));
|
||||
let promise = new Promise<Server>((resolve) => {
|
||||
resolve(server);
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
return this.onReady(() => this.indexedDbService.get().getByKey(this.tablename, id)) as Promise<Server>;
|
||||
}
|
||||
|
||||
public create(server: Server) {
|
||||
if (this.isIncognitoMode) {
|
||||
server.id = this.serverIdsInIncognitoMode.length + 1;
|
||||
localStorage.setItem(`server-${server.id}`, JSON.stringify(server));
|
||||
this.serverIdsInIncognitoMode.push(`server-${server.id}`);
|
||||
server.id = this.serverIds.length + 1;
|
||||
localStorage.setItem(`server-${server.id}`, JSON.stringify(server));
|
||||
|
||||
let promise = new Promise<Server>((resolve) => {
|
||||
resolve(server);
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
this.serverIds.push(`server-${server.id}`);
|
||||
this.updateServerIds();
|
||||
|
||||
return this.onReady(() => {
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
this.indexedDbService
|
||||
.get()
|
||||
.add(this.tablename, server)
|
||||
.then((added) => {
|
||||
server.id = added.key;
|
||||
resolve(server);
|
||||
}, reject);
|
||||
});
|
||||
return promise;
|
||||
let promise = new Promise<Server>((resolve) => {
|
||||
resolve(server);
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
public update(server: Server) {
|
||||
if (this.isIncognitoMode) {
|
||||
localStorage.removeItem(`server-${server.id}`);
|
||||
localStorage.setItem(`server-${server.id}`, JSON.stringify(server));
|
||||
localStorage.removeItem(`server-${server.id}`);
|
||||
localStorage.setItem(`server-${server.id}`, JSON.stringify(server));
|
||||
|
||||
let promise = new Promise<Server>((resolve) => {
|
||||
resolve(server);
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
return this.onReady(() => {
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
this.indexedDbService
|
||||
.get()
|
||||
.update(this.tablename, server)
|
||||
.then((updated) => {
|
||||
resolve(server);
|
||||
}, reject);
|
||||
});
|
||||
return promise;
|
||||
let promise = new Promise<Server>((resolve) => {
|
||||
resolve(server);
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
public findAll() {
|
||||
if (this.isIncognitoMode) {
|
||||
let promise = new Promise<Server[]>((resolve) => {
|
||||
let servers: Server[] = [];
|
||||
this.serverIdsInIncognitoMode.forEach((n) => {
|
||||
let server: Server = JSON.parse(localStorage.getItem(n));
|
||||
servers.push(server);
|
||||
});
|
||||
resolve(servers);
|
||||
let promise = new Promise<Server[]>((resolve) => {
|
||||
let servers: Server[] = [];
|
||||
this.serverIds.forEach((n) => {
|
||||
let server: Server = JSON.parse(localStorage.getItem(n));
|
||||
servers.push(server);
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
return this.onReady(() => this.indexedDbService.get().getAll(this.tablename)) as Promise<Server[]>;
|
||||
resolve(servers);
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
public delete(server: Server) {
|
||||
if (this.isIncognitoMode) {
|
||||
localStorage.removeItem(`server-${server.id}`);
|
||||
this.serverIdsInIncognitoMode = this.serverIdsInIncognitoMode.filter((n) => n !== `server-${server.id}`);
|
||||
localStorage.removeItem(`server-${server.id}`);
|
||||
this.serverIds = this.serverIds.filter((n) => n !== `server-${server.id}`);
|
||||
this.updateServerIds();
|
||||
|
||||
let promise = new Promise((resolve) => {
|
||||
resolve(server.id);
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
return this.onReady(() => this.indexedDbService.get().delete(this.tablename, server.id));
|
||||
let promise = new Promise((resolve) => {
|
||||
resolve(server.id);
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
public getServerUrl(server: Server) {
|
||||
@ -179,25 +117,4 @@ export class ServerService {
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
protected onReady(query) {
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
this.ready.then(
|
||||
() => {
|
||||
query().then(
|
||||
(result) => {
|
||||
resolve(result);
|
||||
},
|
||||
(error) => {
|
||||
reject(error);
|
||||
}
|
||||
);
|
||||
},
|
||||
(error) => {
|
||||
reject(error);
|
||||
}
|
||||
);
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
|
@ -1,101 +0,0 @@
|
||||
import { fakeAsync, inject, TestBed } from '@angular/core/testing';
|
||||
import { PersistenceService, StorageType } from 'angular-persistence';
|
||||
import { Settings, SettingsService } from './settings.service';
|
||||
|
||||
export class MockedSettingsService {
|
||||
settings = {};
|
||||
|
||||
isExperimentalEnabled() {
|
||||
return true;
|
||||
}
|
||||
getAll() {}
|
||||
|
||||
get(key: string) {
|
||||
return this.settings[key];
|
||||
}
|
||||
|
||||
set(key: string, value: any) {
|
||||
this.settings[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
describe('SettingsService', () => {
|
||||
let persistenceService: PersistenceService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [SettingsService, PersistenceService],
|
||||
});
|
||||
|
||||
persistenceService = TestBed.get(PersistenceService);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
persistenceService.removeAll(StorageType.LOCAL);
|
||||
});
|
||||
|
||||
it('should be created', inject([SettingsService], (service: SettingsService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should set value', inject([SettingsService], (service: SettingsService) => {
|
||||
service.set('crash_reports', false);
|
||||
expect(service.get('crash_reports')).toEqual(false);
|
||||
}));
|
||||
|
||||
it('should get default value', inject([SettingsService], (service: SettingsService) => {
|
||||
expect(service.get('crash_reports')).toEqual(true);
|
||||
}));
|
||||
|
||||
it('should throw error when setting value with wrong key', inject([SettingsService], (service: SettingsService) => {
|
||||
expect(() => service.set('test', false)).toThrowError("Key 'test' doesn't exist in settings");
|
||||
}));
|
||||
|
||||
it('should throw error when getting value with wrong key', inject([SettingsService], (service: SettingsService) => {
|
||||
expect(() => service.get('test')).toThrowError("Key 'test' doesn't exist in settings");
|
||||
}));
|
||||
|
||||
it('should get all values', inject([SettingsService], (service: SettingsService) => {
|
||||
expect(service.getAll()).toEqual({
|
||||
crash_reports: true,
|
||||
experimental_features: false,
|
||||
angular_map: false,
|
||||
console_command: undefined,
|
||||
});
|
||||
}));
|
||||
|
||||
it('should set all values', inject([SettingsService], (service: SettingsService) => {
|
||||
const settings = {
|
||||
crash_reports: false,
|
||||
};
|
||||
service.setAll(settings);
|
||||
|
||||
expect(service.getAll()).toEqual({
|
||||
crash_reports: false,
|
||||
experimental_features: false,
|
||||
angular_map: false,
|
||||
console_command: undefined,
|
||||
});
|
||||
}));
|
||||
|
||||
it('should execute subscriber', inject(
|
||||
[SettingsService],
|
||||
fakeAsync((service: SettingsService) => {
|
||||
let changedSettings: Settings;
|
||||
|
||||
service.set('crash_reports', true);
|
||||
service.subscribe((settings) => {
|
||||
changedSettings = settings;
|
||||
});
|
||||
service.set('crash_reports', false);
|
||||
|
||||
expect(changedSettings.crash_reports).toEqual(false);
|
||||
})
|
||||
));
|
||||
|
||||
it('should get isExperimentalEnabled when turned on', inject([SettingsService], (service: SettingsService) => {
|
||||
service.set('experimental_features', true);
|
||||
|
||||
expect(service.isExperimentalEnabled()).toEqual(true);
|
||||
}));
|
||||
});
|
@ -1,11 +1,8 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { PersistenceService, StorageType } from 'angular-persistence';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
export interface Settings {
|
||||
crash_reports: boolean;
|
||||
experimental_features: boolean;
|
||||
angular_map: boolean;
|
||||
console_command: string;
|
||||
}
|
||||
|
||||
@ -13,57 +10,65 @@ export interface Settings {
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class SettingsService {
|
||||
static DEFAULTS: Settings = {
|
||||
private settings: Settings = {
|
||||
crash_reports: true,
|
||||
experimental_features: false,
|
||||
angular_map: false,
|
||||
console_command: undefined,
|
||||
};
|
||||
|
||||
private settingsSubject: BehaviorSubject<Settings>;
|
||||
private readonly reportsSettings: string = 'crash_reports';
|
||||
private readonly consoleSettings: string = 'console_command';
|
||||
|
||||
constructor(private persistenceService: PersistenceService) {
|
||||
this.settingsSubject = new BehaviorSubject<Settings>(this.getAll());
|
||||
constructor() {
|
||||
if (this.getItem(this.reportsSettings))
|
||||
this.settings.crash_reports = this.getItem(this.reportsSettings) === 'true' ? true : false;
|
||||
|
||||
if (this.getItem(this.consoleSettings))
|
||||
this.settings.console_command = this.getItem(this.consoleSettings);
|
||||
}
|
||||
|
||||
get<T>(key: string) {
|
||||
if (!(key in SettingsService.DEFAULTS)) {
|
||||
throw Error(`Key '${key}' doesn't exist in settings`);
|
||||
setReportsSettings(value: boolean) {
|
||||
this.settings.crash_reports = value;
|
||||
this.removeItem(this.reportsSettings);
|
||||
if (value) {
|
||||
this.setItem(this.reportsSettings, 'true');
|
||||
} else {
|
||||
this.setItem(this.reportsSettings, 'false');
|
||||
}
|
||||
const value = this.persistenceService.get(key, StorageType.LOCAL) as T;
|
||||
if (typeof value === 'undefined') {
|
||||
return SettingsService.DEFAULTS[key];
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
set<T>(key: string, value: T): void {
|
||||
if (!(key in SettingsService.DEFAULTS)) {
|
||||
throw Error(`Key '${key}' doesn't exist in settings`);
|
||||
}
|
||||
this.persistenceService.set(key, value, { type: StorageType.LOCAL });
|
||||
this.settingsSubject.next(this.getAll());
|
||||
getReportsSettings() {
|
||||
return this.getItem(this.reportsSettings) === 'true' ? true : false;
|
||||
}
|
||||
|
||||
setConsoleSettings(value: string) {
|
||||
this.settings.console_command = value;
|
||||
this.removeItem(this.consoleSettings);
|
||||
this.setItem(this.consoleSettings, value);
|
||||
}
|
||||
|
||||
getConsoleSettings() {
|
||||
return this.getItem(this.consoleSettings);
|
||||
}
|
||||
|
||||
removeItem(key: string) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
|
||||
setItem(key: string, item: string) {
|
||||
localStorage.setItem(key, item);
|
||||
}
|
||||
|
||||
getItem(item: string) {
|
||||
return localStorage.getItem(item);
|
||||
}
|
||||
|
||||
getAll() {
|
||||
const settings = { ...SettingsService.DEFAULTS };
|
||||
Object.keys(SettingsService.DEFAULTS).forEach((key) => {
|
||||
settings[key] = this.get(key);
|
||||
});
|
||||
return settings;
|
||||
return this.settings;
|
||||
}
|
||||
|
||||
setAll(settings) {
|
||||
Object.keys(settings).forEach((key) => {
|
||||
this.set(key, settings[key]);
|
||||
});
|
||||
}
|
||||
|
||||
isExperimentalEnabled(): boolean {
|
||||
return this.get('experimental_features');
|
||||
}
|
||||
|
||||
subscribe(subscriber: (settings: Settings) => void) {
|
||||
return this.settingsSubject.subscribe(subscriber);
|
||||
setAll(settings: Settings) {
|
||||
this.settings = settings;
|
||||
this.setConsoleSettings(settings.console_command);
|
||||
this.setReportsSettings(settings.crash_reports);
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,16 @@
|
||||
import { MockedSettingsService } from '../settings.service.spec';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { SettingsService } from '../settings.service';
|
||||
import { ConsoleService } from './console.service';
|
||||
|
||||
describe('ConsoleService', () => {
|
||||
let service: ConsoleService;
|
||||
let settings: MockedSettingsService;
|
||||
let settings: SettingsService;
|
||||
|
||||
beforeEach(() => {
|
||||
let defaultConsoleService = {
|
||||
get: () => 'default',
|
||||
};
|
||||
settings = new MockedSettingsService();
|
||||
settings = TestBed.inject(SettingsService);
|
||||
service = new ConsoleService(defaultConsoleService as any, settings as any);
|
||||
});
|
||||
|
||||
@ -18,12 +19,12 @@ describe('ConsoleService', () => {
|
||||
});
|
||||
|
||||
it('should get command from settings if defined', () => {
|
||||
settings.set('console_command', 'from_settings');
|
||||
settings.setConsoleSettings('from_settings');
|
||||
expect(service.command).toEqual('from_settings');
|
||||
});
|
||||
|
||||
it('should get command from default console if settings are not defined', () => {
|
||||
settings.set('console_command', undefined);
|
||||
expect(service.command).toEqual('default');
|
||||
settings.setConsoleSettings(undefined);
|
||||
expect(service.command).toBe('undefined');
|
||||
});
|
||||
});
|
||||
|
@ -7,7 +7,7 @@ export class ConsoleService {
|
||||
constructor(private defaultConsoleService: DefaultConsoleService, private settingsService: SettingsService) {}
|
||||
|
||||
get command(): string {
|
||||
const command = this.settingsService.get<string>('console_command');
|
||||
const command = this.settingsService.getConsoleSettings();
|
||||
if (command === undefined) {
|
||||
return this.defaultConsoleService.get();
|
||||
}
|
||||
@ -15,6 +15,6 @@ export class ConsoleService {
|
||||
}
|
||||
|
||||
set command(command: string) {
|
||||
this.settingsService.set<string>('console_command', command);
|
||||
this.settingsService.setConsoleSettings(command);
|
||||
}
|
||||
}
|
||||
|
@ -49,19 +49,11 @@ describe('SymbolService', () => {
|
||||
expect(req.request.method).toEqual('GET');
|
||||
}));
|
||||
|
||||
it('should load symbols', inject([SymbolService], (service: SymbolService) => {
|
||||
spyOn(service, 'load').and.returnValue(of([]));
|
||||
it('should call load symbols while adding symbol', inject([SymbolService], (service: SymbolService) => {
|
||||
spyOn(service, 'load');
|
||||
|
||||
service.list(server).subscribe();
|
||||
service.add(server, 'symbolName', 'symbol');
|
||||
|
||||
expect(service.load).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should get symbols', inject([SymbolService], (service: SymbolService) => {
|
||||
const symbol = new Symbol();
|
||||
symbol.symbol_id = 'myid';
|
||||
service.symbols.next([symbol]);
|
||||
|
||||
expect(service.get('myid').symbol_id).toEqual('myid');
|
||||
}));
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { BehaviorSubject, Observable, Subject } from 'rxjs';
|
||||
import { shareReplay } from 'rxjs/operators';
|
||||
import { Node } from '../cartography/models/node';
|
||||
import { Server } from '../models/server';
|
||||
@ -7,22 +7,29 @@ import { Symbol } from '../models/symbol';
|
||||
import { HttpServer } from './http-server.service';
|
||||
import { Template } from '../models/template';
|
||||
|
||||
const CACHE_SIZE = 1;
|
||||
|
||||
@Injectable()
|
||||
export class SymbolService {
|
||||
public symbols: BehaviorSubject<Symbol[]> = new BehaviorSubject<Symbol[]>([]);
|
||||
private cache: Observable<Symbol[]>;
|
||||
private symbols: Symbol[] = [];
|
||||
private maximumSymbolSize: number = 80;
|
||||
public symbolsLoaded: Subject<boolean> = new Subject<boolean>();
|
||||
|
||||
constructor(private httpServer: HttpServer) {}
|
||||
|
||||
async load(server: Server) {
|
||||
let symbols = await this.httpServer.get<Symbol[]>(server, '/symbols').toPromise();
|
||||
await symbols.forEach(async symbol => {
|
||||
symbol.raw = await this.raw(server, symbol.symbol_id).toPromise();
|
||||
this.symbols.push(symbol);
|
||||
});
|
||||
this.symbolsLoaded.next(true);
|
||||
}
|
||||
|
||||
getMaximumSymbolSize() {
|
||||
return this.maximumSymbolSize;
|
||||
}
|
||||
|
||||
get(symbol_id: string): Symbol {
|
||||
return this.symbols.getValue().find((symbol: Symbol) => symbol.symbol_id === symbol_id);
|
||||
return this.symbols.find((symbol: Symbol) => symbol.filename === symbol_id);
|
||||
}
|
||||
|
||||
getDimensions(server: Server, symbol_id: string): Observable<SymbolDimension> {
|
||||
@ -38,25 +45,13 @@ export class SymbolService {
|
||||
};
|
||||
}
|
||||
|
||||
getByFilename(symbol_filename: string) {
|
||||
return this.symbols.getValue().find((symbol: Symbol) => symbol.filename === symbol_filename);
|
||||
}
|
||||
|
||||
add(server: Server, symbolName: string, symbol: string) {
|
||||
this.cache = null;
|
||||
this.load(server);
|
||||
return this.httpServer.post(server, `/symbols/${symbolName}/raw`, symbol);
|
||||
}
|
||||
|
||||
load(server: Server): Observable<Symbol[]> {
|
||||
return this.httpServer.get<Symbol[]>(server, '/symbols');
|
||||
}
|
||||
|
||||
list(server: Server) {
|
||||
if (!this.cache) {
|
||||
this.cache = this.load(server).pipe(shareReplay(CACHE_SIZE));
|
||||
}
|
||||
|
||||
return this.cache;
|
||||
return this.httpServer.get<Symbol[]>(server, '/symbols');
|
||||
}
|
||||
|
||||
raw(server: Server, symbol_id: string) {
|
||||
@ -64,8 +59,8 @@ export class SymbolService {
|
||||
return this.httpServer.getText(server, `/symbols/${encoded_uri}/raw`);
|
||||
}
|
||||
|
||||
getSymbolFromTemplate(server: Server, template: Template) {
|
||||
return `${server.protocol}//${server.host}:${server.port}/v3/symbols/${template.symbol}/raw`;
|
||||
getSymbolFromTemplate(template: Template) {
|
||||
return this.symbols.find((symbol: Symbol) => symbol.filename === template.symbol.split('/')[2]);
|
||||
}
|
||||
}
|
||||
|
||||
|
16
src/app/services/user.service.ts
Normal file
16
src/app/services/user.service.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { Server } from '../models/server';
|
||||
import { HttpServer } from './http-server.service';
|
||||
import { User } from '../models/users/user';
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
constructor(
|
||||
private httpServer: HttpServer
|
||||
) {}
|
||||
|
||||
getInformationAboutLoggedUser(server: Server) {
|
||||
return this.httpServer.get<User>(server, '/users/me/');
|
||||
}
|
||||
}
|
@ -16,4 +16,4 @@ export const environment = {
|
||||
* This import should be commented out in production mode because it will have a negative impact
|
||||
* on performance if an error is thrown.
|
||||
*/
|
||||
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.
|
||||
// import 'zone.js/plugins/zone-error'; // Included with Angular CLI.
|
||||
|
@ -35,7 +35,7 @@
|
||||
</script>
|
||||
</head>
|
||||
<!-- <body class="mat-app-background" oncontextmenu="return false;"> -->
|
||||
<body class="mat-app-background">
|
||||
<body class="mat-app-background" oncontextmenu="return false;">
|
||||
<app-root></app-root>
|
||||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-5D6FZL9923"></script>
|
||||
|
@ -72,7 +72,7 @@
|
||||
/***************************************************************************************************
|
||||
* Zone JS is required by default for Angular itself.
|
||||
*/
|
||||
import 'zone.js/dist/zone'; // Included with Angular CLI.
|
||||
import 'zone.js'; // Included with Angular CLI.
|
||||
|
||||
/***************************************************************************************************
|
||||
* APPLICATION IMPORTS
|
||||
|
@ -1,4 +1,4 @@
|
||||
import 'zone.js/dist/zone-testing';
|
||||
import 'zone.js/testing';
|
||||
import { getTestBed } from '@angular/core/testing';
|
||||
import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';
|
||||
|
||||
|
@ -3,6 +3,13 @@
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/app",
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@components/*": ["app/components/*"],
|
||||
"@services/*": ["app/services/*"],
|
||||
"@resolvers/*": ["app/resolvers/*"],
|
||||
"@filters/*": ["app/filters/*"],
|
||||
"@models/*": ["app/models/*"]
|
||||
},
|
||||
"types": []
|
||||
},
|
||||
"files": [
|
||||
@ -10,7 +17,6 @@
|
||||
"polyfills.ts"
|
||||
],
|
||||
"include": [
|
||||
// "../src/**/*",
|
||||
"../node_modules/angular2-indexeddb/*"
|
||||
// "../src/**/*"
|
||||
]
|
||||
}
|
||||
|
@ -15,7 +15,6 @@
|
||||
],
|
||||
"include": [
|
||||
"**/*.spec.ts",
|
||||
"**/*.d.ts",
|
||||
"../node_modules/angular2-indexeddb/*"
|
||||
"**/*.d.ts"
|
||||
]
|
||||
}
|
||||
|
@ -8,11 +8,9 @@
|
||||
"declaration": false,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
"target": "es5",
|
||||
"jsx": "react",
|
||||
"types": ["jasmine", "jest", "mocha", "node"],
|
||||
"typeRoots": [
|
||||
"node_modules/@types"
|
||||
|
Loading…
Reference in New Issue
Block a user