diff options
25 files changed, 1370 insertions, 68 deletions
diff --git a/.vscode/settings.json b/.vscode/settings.json index 47bcd75..c1d3b4d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,8 @@ // Place your settings in this file to overwrite default and user settings. { + // Specify GOPATH here to override the one that is set as environment variable. The inferred GOPATH from workspace root overrides this, if go.inferGopath is set to true. + "go.gopath": "${workspaceRoot}/../../../../../..", + // Controls the rendering size of tabs in characters. // If set to auto, the value will be guessed based on the opened file. "editor.tabSize": 2, @@ -31,67 +34,74 @@ ], // Words to add to dictionary for a workspace. "cSpell.words": [ - "apiv", - "gonic", - "devel", - "csrffound", - "Syncthing", - "STID", + "CIFS", + "Checkboxes", + "EVTSDK", + "EXEPATH", + "Flds", + "Grafana", + "IPROJECT", "ISTCONFIG", - "socketio", - "ldflags", - "SThg", - "stconfig", - "Intf", - "dismissible", - "rpath", - "WSID", - "sess", "IXDS", - "golib", - "xdsapi", - "xdsconfig", - "xdsserver", - "xdsagent", - "nbsp", "Inot", - "inotify", - "cmdi", - "sdkid", - "Flds", - "prjs", - "iosk", - "CIFS", - "IPROJECT", - "unregister", - "conv", + "Intf", "PATHMAP", - "nospace", - "graphx", + "STID", + "SThg", + "Sillyf", + "Syncthing", "Truthy", + "WSID", + "XDSSUPERV", + "abortinstall", + "apiv", + "cmdi", + "conv", + "csrffound", "darkviolet", + "devel", + "dismissible", "dwnl", - "topnav", - "leftbar", - "urfave", - "unmarshall", - "sebd", - "priv", "evts", + "franciscocpg", "gdbserver", - "tabset", + "gerrit", + "golib", + "gonic", + "graphx", + "inotify", + "iosk", + "ldflags", + "leftbar", + "nbsp", + "nospace", "pageview", - "subpath", "prebuild", + "priv", + "prjs", "reflectme", - "franciscocpg", - "xsapiv", - "xaapiv", - "Sillyf", + "rpath", + "sdkid", + "sebd", + "sess", + "socketio", + "stconfig", + "subpath", "tabindex", - "EVTSDK", - "gerrit", - "tgts" + "tabset", + "tgts", + "topnav", + "topo", + "unmarshall", + "unregister", + "urfave", + "xaapiv", + "xdsagent", + "xdsapi", + "xdsconfig", + "xdspvr", + "xdsserver", + "xsapiv" ], // codelyzer "tslint.rulesDirectory": "./webapp/node_modules/codelyzer", diff --git a/lib/agent/agent.go b/lib/agent/agent.go index 3aa89a8..58f336c 100644 --- a/lib/agent/agent.go +++ b/lib/agent/agent.go @@ -47,11 +47,12 @@ type Context struct { SThgCmd *exec.Cmd SThgInotCmd *exec.Cmd - webServer *WebServer - xdsServers map[string]*XdsServer - sessions *Sessions - events *Events - projects *Projects + webServer *WebServer + xdsServers map[string]*XdsServer + XdsSupervisor *XdsSupervisor + sessions *Sessions + events *Events + projects *Projects Exit chan os.Signal } diff --git a/lib/agent/apiv1-supervisor.go b/lib/agent/apiv1-supervisor.go new file mode 100644 index 0000000..a34a913 --- /dev/null +++ b/lib/agent/apiv1-supervisor.go @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2017-2018 "IoT.bzh" + * Author Sebastien Douheret <sebastien@iot.bzh> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package agent + +import ( + "net/http" + + common "gerrit.automotivelinux.org/gerrit/src/xds/xds-common.git/golib" + "github.com/gin-gonic/gin" +) + +// getSupervisorTopo : return current AGL daemons topology using supervisor +func (s *APIService) getSupervisorTopo(c *gin.Context) { + + xdspvr, err := s._initXdsSupervisor() + if err != nil { + common.APIError(c, err.Error()) + return + } + + var res XdsSuperVReply + if err = xdspvr.GetTopo(&res); err != nil { + common.APIError(c, err.Error()) + return + } + + if res.Request.Status != "success" { + common.APIError(c, res.Request.Info) + return + } + + c.JSON(http.StatusOK, res.Response) +} + +// startSupervisor : resquest to supervisor to start tracing +func (s *APIService) startSupervisor(c *gin.Context) { + + xdspvr, err := s._initXdsSupervisor() + if err != nil { + common.APIError(c, err.Error()) + return + } + + var cfg XdsSuperVTraceConfig + if c.BindJSON(&cfg) != nil { + common.APIError(c, "Invalid config argument") + return + } + s.Log.Debugf("Start Supervisor cfgArg %v\n", cfg) + + var res XdsSuperVReply + if err = xdspvr.StartTrace(cfg, &res); err != nil { + common.APIError(c, err.Error()) + return + } + + if res.Request.Status != "success" { + common.APIError(c, res.Request.Info) + return + } + + c.JSON(http.StatusOK, res.Response) +} + +// stopSupervisor : resquest to supervisor to stop tracing +func (s *APIService) stopSupervisor(c *gin.Context) { + + xdspvr, err := s._initXdsSupervisor() + if err != nil { + common.APIError(c, err.Error()) + return + } + + var res XdsSuperVReply + if err = xdspvr.StopTrace(&res); err != nil { + common.APIError(c, err.Error()) + return + } + + if res.Request.Status != "success" { + common.APIError(c, res.Request.Info) + return + } + + c.JSON(http.StatusOK, res.Response) +} + +// _initXdsSupervisor . +func (s *APIService) _initXdsSupervisor() (*XdsSupervisor, error) { + + if s.XdsSupervisor == nil { + xs := NewXdsSupervisor(s.Context) + if err := xs.Connect(); err != nil { + return nil, err + } + s.XdsSupervisor = xs + } + return s.XdsSupervisor, nil +} diff --git a/lib/agent/apiv1.go b/lib/agent/apiv1.go index 97165b3..504558e 100644 --- a/lib/agent/apiv1.go +++ b/lib/agent/apiv1.go @@ -67,6 +67,9 @@ func NewAPIV1(ctx *Context) *APIService { s.apiRouter.POST("/events/register", s.eventsRegister) s.apiRouter.POST("/events/unregister", s.eventsUnRegister) + s.apiRouter.GET("/supervisor/topo", s.getSupervisorTopo) + s.apiRouter.POST("/supervisor/trace/start", s.startSupervisor) + s.apiRouter.POST("/supervisor/trace/stop", s.stopSupervisor) return s } diff --git a/lib/agent/xdsserver.go b/lib/agent/xdsserver.go index f74e3ba..24e51d7 100644 --- a/lib/agent/xdsserver.go +++ b/lib/agent/xdsserver.go @@ -157,7 +157,7 @@ func (xs *XdsServer) SetLoggerOutput(out io.Writer) { xs.logOut = out } -// GetConfig +// GetConfig return the current server config func (xs *XdsServer) GetConfig() xaapiv1.ServerCfg { return xaapiv1.ServerCfg{ ID: xs.ID, diff --git a/lib/agent/xdssupervior.go b/lib/agent/xdssupervior.go new file mode 100644 index 0000000..bbe2500 --- /dev/null +++ b/lib/agent/xdssupervior.go @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2017-2018 "IoT.bzh" + * Author Sebastien Douheret <sebastien@iot.bzh> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package agent + +import ( + "fmt" + "io" + "strings" + "time" + + common "gerrit.automotivelinux.org/gerrit/src/xds/xds-common.git/golib" + uuid "github.com/satori/go.uuid" +) + +// XdsSupervisor . +type XdsSupervisor struct { + *Context + ID string + BaseURL string + ConnRetry int + Connected bool + Disabled bool + + // Private fields + client *common.HTTPClient + logOut io.Writer + cbOnConnect OnConnectedXdsSupervCB +} + +// XdsSuperVRequest Resquest field of a reply +type XdsSuperVRequest struct { + Status string `json:"status"` + Info string `json:"info"` +} + +// XdsSuperVReply Reply structure of XDS Supervision Daemon +type XdsSuperVReply struct { + JType string `json:"jtype"` + Request XdsSuperVRequest `json:"request"` + Response interface{} `json:"response"` +} + +// XdsSuperVTraceConfig +type XdsSuperVTraceConfig struct { + Pid int `json:"pid"` + Pids []int `json:"pids"` + WsName string `json:"ws"` +} + +// OnConnectedXdsSupervCB connect callback +type OnConnectedXdsSupervCB func(svr *XdsSupervisor) error + +// NewXdsSupervisor creates an instance of XdsSupervisor +func NewXdsSupervisor(ctx *Context) *XdsSupervisor { + return &XdsSupervisor{ + Context: ctx, + ID: "XdsSupervisor-" + uuid.NewV1().String(), + BaseURL: ctx.Config.FileConf.ProfileConf.XDSBinder.URL, + ConnRetry: ctx.Config.FileConf.ProfileConf.XDSBinder.ConnRetry, + Connected: false, + Disabled: false, + + logOut: ctx.Log.Out, + } +} + +// Connect Establish HTTP connection with XDS Supervisor Dameon +func (xs *XdsSupervisor) Connect() error { + var err error + var retry int + + xs.Disabled = false + xs.Connected = false + + err = nil + for retry = xs.ConnRetry; retry > 0; retry-- { + if err = xs._CreateConnectHTTP(); err == nil { + break + } + if retry == xs.ConnRetry { + // Notify only on the first conn error + // doing that avoid 2 notifs (conn false; conn true) on startup + xs._NotifyState() + } + xs.Log.Infof("Establishing connection to XDS Supervisor daemon (retry %d/%d)", retry, xs.ConnRetry) + time.Sleep(time.Second) + } + if retry == 0 { + // FIXME: re-use _Reconnect to wait longer in background + return fmt.Errorf("Connection to XDS Supervisor daemon failure") + } + if err != nil { + return err + } + + // Check HTTP connection and establish WS connection + err = xs._Connect(false) + + return err +} + +// ConnectOn Register a callback on events reception +func (xs *XdsSupervisor) ConnectOn(f OnConnectedXdsSupervCB) error { + xs.cbOnConnect = f + return nil +} + +// GetVersion Send Get request to retrieve XDS Supervision version +func (xs *XdsSupervisor) GetVersion(res interface{}) error { + // FIXME add suffix URLSuffix in common HTTP client lib instead of _BuildURL + return xs.client.Get(xs._BuildURL("/version"), &res) +} + +// GetTopo Send Get request to retrieve Services/Daemons topology +func (xs *XdsSupervisor) GetTopo(res interface{}) error { + return xs.client.Get(xs._BuildURL("/list"), &res) +} + +// StartTrace Send Supervisor config and start tracing +func (xs *XdsSupervisor) StartTrace(cfg XdsSuperVTraceConfig, res interface{}) error { + return xs.client.Post(xs._BuildURL("/trace/start"), cfg, &res) +} + +// StopTrace Send Supervisor stop tracing +func (xs *XdsSupervisor) StopTrace(res interface{}) error { + var cfg interface{} + return xs.client.Post(xs._BuildURL("/trace/stop"), cfg, res) +} + +/*** +** Private functions +***/ + +// _BuildURL . +func (xs *XdsSupervisor) _BuildURL(url string) string { + return url + "?token=HELLO&uuid=magic" +} + +// Create HTTP client +func (xs *XdsSupervisor) _CreateConnectHTTP() error { + var err error + // FIXME SEB - Client key not in header but in cookie + // temporary workaround: used _BuildURL to append uuid=magic in URL + // map[Set-Cookie:[x-afb-uuid-5678=2b185cc3-276b-4097-91fa-d607eaf937e6; Path=/api; Max-Age=32000000; ... + //port := strings.Split(xs.BaseURL, ":")[2] + //"x-afb-uuid-" + port + + xs.client, err = common.HTTPNewClient(xs.BaseURL, + common.HTTPClientConfig{ + //HeaderClientKeyName: "Xds-Sid", + HeaderAPIKeyName: "token", + Apikey: "HELLO", + URLPrefix: "/api/xds", + CsrfDisable: true, + LogOut: xs.logOut, + LogPrefix: "XDSSUPERV: ", + LogLevel: common.HTTPLogLevelWarning, + }) + + xs.client.SetLogLevel(xs.Log.Level.String()) + + if err != nil { + msg := ": " + err.Error() + if strings.Contains(err.Error(), "connection refused") { + msg = fmt.Sprintf("(url: %s)", xs.BaseURL) + } + return fmt.Errorf("ERROR: cannot connect to XDS Supervisor %s", msg) + } + if xs.client == nil { + return fmt.Errorf("ERROR: cannot connect to XDS Supervisor (null client)") + } + + return nil +} + +// _Connect Established HTTP and WS connection +func (xs *XdsSupervisor) _Connect(reConn bool) error { + + var res interface{} + if err := xs.client.Get(xs._BuildURL("/ping"), &res); err != nil { + xs.Connected = false + if !reConn { + xs._NotifyState() + } + return err + } + + xs.Connected = true + + // Call OnConnect callback + if xs.cbOnConnect != nil { + xs.cbOnConnect(xs) + } + + xs._NotifyState() + return nil +} + +// _NotifyState Send event to notify changes +func (xs *XdsSupervisor) _NotifyState() { + + /* TODO + evSts := xaapiv1.ServerCfg{ + ID: xs.ID, + URL: xs.BaseURL, + APIURL: xs.APIURL, + PartialURL: xs.PartialURL, + ConnRetry: xs.ConnRetry, + Connected: xs.Connected, + } + if err := xs.events.Emit(xaapiv1.EVTServerConfig, evSts, ""); err != nil { + xs.Log.Warningf("Cannot notify XdsServer state change: %v", err) + } + */ +} diff --git a/lib/xdsconfig/config.go b/lib/xdsconfig/config.go index b7bf57b..352ba84 100644 --- a/lib/xdsconfig/config.go +++ b/lib/xdsconfig/config.go @@ -91,6 +91,12 @@ func Init(ctx *cli.Context, log *logrus.Logger) (*Config, error) { SThgConf: &SyncThingConf{ Home: defaultSTHomeDir, }, + ProfileConf: ProfileConfT{ + XDSBinder: XDSBinderConf{ + URL: "http://localhost:5678", + ConnRetry: 10, + }, + }, }, Log: log, } diff --git a/lib/xdsconfig/configfile.go b/lib/xdsconfig/configfile.go index 009517f..1390456 100644 --- a/lib/xdsconfig/configfile.go +++ b/lib/xdsconfig/configfile.go @@ -43,6 +43,15 @@ type XDSServerConf struct { APIPartialURL string `json:"-"` } +type XDSBinderConf struct { + URL string `json:"url"` + ConnRetry int `json:"connRetry"` +} + +type ProfileConfT struct { + XDSBinder XDSBinderConf `json:"xdsBinder"` +} + type FileConfig struct { HTTPPort string `json:"httpPort"` WebAppDir string `json:"webAppDir"` @@ -50,6 +59,7 @@ type FileConfig struct { XDSAPIKey string `json:"xds-apikey"` ServersConf []XDSServerConf `json:"xdsServers"` SThgConf *SyncThingConf `json:"syncthing"` + ProfileConf ProfileConfT `json:"profileConf"` } // readGlobalConfig reads configuration from a config file. diff --git a/webapp/package.json b/webapp/package.json index d79aa91..2df4888 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -52,7 +52,7 @@ "bootstrap": "4.0.0-beta.2", "classlist.js": "1.1.20150312", "core-js": "2.4.1", - "d3": "4.8.0", + "d3": "3.5.17", "font-awesome": "4.7.0", "font-awesome-animation": "0.0.10", "intl": "1.2.5", diff --git a/webapp/src/app/@core-xds/services/@core-xds-services.module.ts b/webapp/src/app/@core-xds/services/@core-xds-services.module.ts index a3a67c5..6a4eb3c 100644 --- a/webapp/src/app/@core-xds/services/@core-xds-services.module.ts +++ b/webapp/src/app/@core-xds/services/@core-xds-services.module.ts @@ -23,6 +23,7 @@ import { AlertService } from './alert.service'; import { ConfigService } from './config.service'; import { ProjectService } from './project.service'; import { SdkService } from './sdk.service'; +import { SupervisionService } from './supervision.service'; import { TargetService } from './target.service'; import { UserService } from './users.service'; import { XDSConfigService } from './xds-config.service'; @@ -33,6 +34,7 @@ const SERVICES = [ ConfigService, ProjectService, SdkService, + SupervisionService, TargetService, UserService, XDSConfigService, diff --git a/webapp/src/app/@core-xds/services/supervision.service.ts b/webapp/src/app/@core-xds/services/supervision.service.ts new file mode 100644 index 0000000..4a9f578 --- /dev/null +++ b/webapp/src/app/@core-xds/services/supervision.service.ts @@ -0,0 +1,61 @@ +/** +* @license +* Copyright (C) 2018 "IoT.bzh" +* Author Sebastien Douheret <sebastien@iot.bzh> +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import { Injectable, SecurityContext, isDevMode } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; + +import { XDSAgentService } from '../services/xdsagent.service'; + +export interface AglTopology { + name: string; + pid: number; + isClient: boolean; + isServer: boolean; + ws_clients: string[]; + ws_servers: string[]; + apis: any; +} + +@Injectable() +export class SupervisionService { + + private curServerID; + + constructor(private xdsSvr: XDSAgentService) { + /* + this.xdsSvr.XdsConfig$.subscribe(cfg => { + if (!cfg || cfg.servers.length < 1) { + return; + } + }); + */ + } + + getTopo(): Observable<AglTopology[]> { + return this.xdsSvr.getTopoSupervisor(); + } + + startTrace(cfg: any): Observable<any> { + return this.xdsSvr.startTraceSupervisor(cfg); + } + + stopTrace(cfg: any): Observable<any> { + return this.xdsSvr.stopTraceSupervisor(cfg); + } + +} diff --git a/webapp/src/app/@core-xds/services/xdsagent.service.ts b/webapp/src/app/@core-xds/services/xdsagent.service.ts index adbee98..002c84b 100644 --- a/webapp/src/app/@core-xds/services/xdsagent.service.ts +++ b/webapp/src/app/@core-xds/services/xdsagent.service.ts @@ -665,6 +665,21 @@ export class XDSAgentService { { cols: cols, rows: rows }); } + /*** + ** Supervision + ***/ + getTopoSupervisor(): Observable<any> { + return this._get('/supervisor/topo'); + } + + startTraceSupervisor(cfg: any): Observable<any> { + return this._post('/supervisor/trace/start', cfg); + } + + stopTraceSupervisor(cfg: any): Observable<any> { + return this._post('/supervisor/trace/stop', cfg); + } + /** ** Private functions ***/ diff --git a/webapp/src/app/pages/confirm/confirm-modal/confirm-modal.component.ts b/webapp/src/app/pages/confirm/confirm-modal/confirm-modal.component.ts index 67a7d87..d3fec86 100644 --- a/webapp/src/app/pages/confirm/confirm-modal/confirm-modal.component.ts +++ b/webapp/src/app/pages/confirm/confirm-modal/confirm-modal.component.ts @@ -17,7 +17,6 @@ */ import { Component, OnInit, Input } from '@angular/core'; -import { DomSanitizer } from '@angular/platform-browser'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; export enum EType { @@ -69,7 +68,6 @@ export class ConfirmModalComponent implements OnInit { constructor( private modalRef: NgbActiveModal, - private sanitizer: DomSanitizer, ) { } ngOnInit() { diff --git a/webapp/src/app/pages/pages-menu.ts b/webapp/src/app/pages/pages-menu.ts index 86884bc..230966d 100644 --- a/webapp/src/app/pages/pages-menu.ts +++ b/webapp/src/app/pages/pages-menu.ts @@ -48,11 +48,6 @@ export const MENU_ITEMS: NbMenuItem[] = [ group: true, }, { - title: 'Projects', - icon: 'nb-keypad', - link: '/pages/projects', - }, - { title: 'SDKs', icon: 'fa fa-file-archive-o', link: '/pages/sdks', @@ -66,6 +61,16 @@ export const MENU_ITEMS: NbMenuItem[] = [ */ }, { + title: 'Projects', + icon: 'nb-keypad', + link: '/pages/projects', + }, + { + title: 'Build', + icon: 'fa fa-cogs', + link: '/pages/build', + }, + { title: 'Targets', icon: 'fa fa-microchip', link: '/pages/targets', @@ -81,9 +86,19 @@ export const MENU_ITEMS: NbMenuItem[] = [ ], }, { - title: 'Build', - icon: 'fa fa-cogs', - link: '/pages/build', + title: 'Supervision / Monitoring', + icon: 'fa fa-bar-chart', + link: '/pages/supervision', + children: [ + { + title: 'Config', + link: '/pages/supervision/config', + }, + { + title: 'Graph', + link: '/pages/supervision/graph', + }, + ], }, { title: 'MISC', diff --git a/webapp/src/app/pages/pages-routing.module.ts b/webapp/src/app/pages/pages-routing.module.ts index 655dea2..ae2ef4a 100644 --- a/webapp/src/app/pages/pages-routing.module.ts +++ b/webapp/src/app/pages/pages-routing.module.ts @@ -27,6 +27,8 @@ import { SdkManagementComponent } from './sdks/sdk-management/sdk-management.com import { TargetsComponent } from './targets/targets.component'; import { TerminalsComponent } from './targets/terminals/terminals.component'; import { BuildComponent } from './build/build.component'; +import { SupervisionComponent } from './supervision/supervision.component'; +import { SupervisionConfigComponent } from './supervision/supervision-config.component'; const routes: Routes = [{ path: '', @@ -53,6 +55,12 @@ const routes: Routes = [{ path: 'targets/term', component: TerminalsComponent, }, { + path: 'supervision/config', + component: SupervisionConfigComponent, + }, { + path: 'supervision/graph', + component: SupervisionComponent, + }, { path: 'config', loadChildren: './config/config.module#ConfigModule', }, diff --git a/webapp/src/app/pages/pages.module.ts b/webapp/src/app/pages/pages.module.ts index 55fe61a..5ffa8d6 100644 --- a/webapp/src/app/pages/pages.module.ts +++ b/webapp/src/app/pages/pages.module.ts @@ -26,6 +26,7 @@ import { DashboardModule } from './dashboard/dashboard.module'; import { BuildModule } from './build/build.module'; import { ProjectsModule } from './projects/projects.module'; import { SdksModule } from './sdks/sdks.module'; +import { SupervisionModule } from './supervision/supervision.module'; import { TargetsModule } from './targets/targets.module'; import { PagesRoutingModule } from './pages-routing.module'; import { NotificationsComponent } from './notifications/notifications.component'; @@ -48,6 +49,7 @@ const PAGES_COMPONENTS = [ SdksModule, ToasterModule, TargetsModule, + SupervisionModule, ], declarations: [ ...PAGES_COMPONENTS, diff --git a/webapp/src/app/pages/sdks/sdk-management/sdk-install.component.ts b/webapp/src/app/pages/sdks/sdk-management/sdk-install.component.ts index 1957c8b..c21cb66 100644 --- a/webapp/src/app/pages/sdks/sdk-management/sdk-install.component.ts +++ b/webapp/src/app/pages/sdks/sdk-management/sdk-install.component.ts @@ -17,7 +17,6 @@ */ import { Component, OnInit, Input, ViewChild, AfterViewChecked, ElementRef } from '@angular/core'; -import { DomSanitizer } from '@angular/platform-browser'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { AlertService } from '../../../@core-xds/services/alert.service'; @@ -73,7 +72,6 @@ export class SdkInstallComponent implements OnInit { constructor( private modalRef: NgbActiveModal, - private sanitizer: DomSanitizer, private alert: AlertService, private sdkSvr: SdkService, ) { } diff --git a/webapp/src/app/pages/supervision/supervision-config.component.html b/webapp/src/app/pages/supervision/supervision-config.component.html new file mode 100644 index 0000000..1fbcd70 --- /dev/null +++ b/webapp/src/app/pages/supervision/supervision-config.component.html @@ -0,0 +1,45 @@ +<div class="row"> + <h3 style="margin-top: auto; margin-bottom: auto">Configuration</h3> + + <div class="row"> + <div class="col-md-12"> + <nb-card-body> + <div class="col-md-9"> + <nb-actions size="small"> + <nb-action> + <button id="refresh-topo" (click)="getAGLTopo()"> + <i class="fa fa-refresh"></i> + </button> + </nb-action> + </nb-actions> + </div> + </nb-card-body> + </div> + </div> +</div> +<div class="row"> + <div class="col-md-10"> + <svg id="graph" width="100%" height="500"> + </svg> + </div> + <div class="col-md-2"> + <div> + <label>Daemons to monitor</label> + </div> + <nb-checkbox *ngFor="let wsCkx of daemonCheckboxes" [disabled]="wsCkx.disabled" [(ngModel)]="wsCkx.value">{{wsCkx.name}} + </nb-checkbox> + <div style="margin-top: 20px;"> + <div> + <label>Monitoring actions:</label> + </div> + <button id="start-trace" class="btn btn-primary" (click)="onStartTrace()" [disabled]=" + isStartBtnDisable()">{{ starting ?"Starting... ":"Start" }} + <span *ngIf="starting" class="fa fa-gear faa-spin animated fa-size-x2"></span> + </button> + <button id="stop-trace" class="btn btn-primary" (click)="onStopTrace()" [disabled]=" + isStopBtnDisable()">{{ stopping ?"Stopping... ":"Stop" }} + <span *ngIf="stopping" class="fa fa-gear faa-spin animated fa-size-x2"></span> + </button> + </div> + </div> +</div> diff --git a/webapp/src/app/pages/supervision/supervision-config.component.scss b/webapp/src/app/pages/supervision/supervision-config.component.scss new file mode 100644 index 0000000..ea263cc --- /dev/null +++ b/webapp/src/app/pages/supervision/supervision-config.component.scss @@ -0,0 +1,139 @@ +/* FIXME: not working due to workaround (ViewEncapsulation.None) for svg +@import '../../@theme/styles/themes'; +@import '~@nebular/theme/components/card/card.component.theme'; +@import '~bootstrap/scss/mixins/breakpoints'; +@import '~@nebular/theme/styles/global/bootstrap/breakpoints'; +@include nb-install-component() { + nb-card-body { + display: flex; + align-items: center; + } + .action-groups-header { + flex-basis: 20%; + color: nb-theme(card-header-fg-heading); + font-family: nb-theme(card-header-font-family); + font-size: nb-theme(card-header-font-size); + font-weight: nb-theme(card-header-font-weight); + } + .nb-actions { + flex-basis: 80%; + } + .right { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + order: 1; + flex-direction: row-reverse; + } + nb-actions > nb-action { + padding: 0; + } + nb-action { + i { + color: nb-theme(color-fg); + font-size: 1.5rem; + margin-right: 1rem; + } + span { + font-family: nb-theme(font-secondary); + font-weight: nb-theme(font-weight-bold); + color: nb-theme(color-fg-heading); + text-transform: uppercase; + } + button { + margin: 0 auto; + padding: 0; + cursor: pointer; + border: none; + background: none; + display: flex; + align-items: center; + &:focus { + box-shadow: none; + outline: none; + } + } + } + @include media-breakpoint-down(md) { + nb-actions nb-action { + padding: 0 0.75rem; + } + } + @include media-breakpoint-down(sm) { + nb-card-body { + padding: 1rem; + } + nb-action { + font-size: 0.75rem; + i { + font-size: 2rem; + margin-right: 0.5rem; + } + } + } + @include media-breakpoint-down(is) { + nb-action i { + font-size: 1.75rem; + margin: 0; + } + span { + display: none; + } + } +} +*/ + +button#refresh-topo { + color: #a4abb3; + font-size: 1.5rem; + margin-right: 1rem; + margin: 0 auto; + padding: 0; + cursor: pointer; + border: none; + background: none; + display: flex; + align-items: center; + &:focus { + box-shadow: none; + outline: none; + } +} + +button#start-trace { + margin-top: 10px; + margin-left: 10px; +} + +button#stop-trace { + margin-top: 10px; + margin-left: 10px; +} + +svg#graph { + .link { + fill: none; + stroke: #666; + stroke-width: 1.5px; + } + #ws-client { + fill: green; + } + .link.ws-client { + stroke: green; + } + .link.not-connected { + stroke-dasharray: 0, 2 1; + } + circle { + fill: #ccc; + stroke: #333; + stroke-width: 1.5px; + } + text { + font: 1rem sans-serif; + pointer-events: none; + text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff; + } +} diff --git a/webapp/src/app/pages/supervision/supervision-config.component.ts b/webapp/src/app/pages/supervision/supervision-config.component.ts new file mode 100644 index 0000000..e96b936 --- /dev/null +++ b/webapp/src/app/pages/supervision/supervision-config.component.ts @@ -0,0 +1,305 @@ +/** +* @license +* Copyright (C) 2017-2018 "IoT.bzh" +* Author Sebastien Douheret <sebastien@iot.bzh> +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import { Component, OnInit, AfterViewInit, ViewEncapsulation } from '@angular/core'; +import { Injectable, Inject } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import * as d3 from 'd3'; + +import { SupervisionService, AglTopology } from '../../@core-xds/services/supervision.service'; +import { AlertService } from '../../@core-xds/services/alert.service'; + +interface WsCheckbox { + name: string; + pid: number; + value: boolean; + disabled: boolean; + tooltip: string; +} + +@Component({ + selector: 'xds-supervision', + styleUrls: ['./supervision-config.component.scss'], + templateUrl: './supervision-config.component.html', + encapsulation: ViewEncapsulation.None, // workaround about https://github.com/angular/angular/issues/7845 +}) +export class SupervisionConfigComponent implements OnInit, AfterViewInit { + + daemonCheckboxes: WsCheckbox[] = []; + starting = false; + stopping = false; + + private graph: any; + private svg: any; + private links = []; + + constructor(@Inject(DOCUMENT) private document: Document, + private supervisorSvr: SupervisionService, + private alert: AlertService, + ) { + + } + + ngOnInit() { + + } + + ngAfterViewInit() { + this.getAGLTopo(); + } + + getAGLTopo() { + this.supervisorSvr.getTopo().subscribe(topo => { + this.graphAGLBindings(topo); + this.updateCheckboxes(topo); + }); + } + + onStartTrace() { + this.starting = true; + + const dmArr = []; + this.daemonCheckboxes.forEach(dm => dm.value && dmArr.push(dm.pid)); + + this.supervisorSvr.startTrace({ pids: dmArr }).subscribe(res => { + this.starting = false; + this.alert.info('Monitoring successfully started'); + }, err => { + this.starting = false; + this.alert.error(err); + }); + } + + onStopTrace() { + this.stopping = true; + this.supervisorSvr.stopTrace({}).subscribe(res => { + this.stopping = false; + this.alert.info('Monitoring successfully stopped'); + }, err => { + this.stopping = false; + this.alert.error(err); + }); + } + + isStartBtnDisable(): boolean { + return this.starting; + } + + isStopBtnDisable(): boolean { + return this.stopping; + } + + private updateCheckboxes(topo: AglTopology[]) { + this.daemonCheckboxes = []; + topo.forEach(elem => { + this.daemonCheckboxes.push({ + name: elem.name, + pid: elem.pid, + value: false, + disabled: false, + tooltip: 'Daemon ' + elem.name + ' (pid ' + elem.pid + ')', + }); + }); + + } + + + // Compute the distinct nodes from the links. + // Based on http://bl.ocks.org/mbostock/1153292 + private graphAGLBindings(topo: AglTopology[]) { + + const ws_link: { [id: string]: string[] } = {}; + let ii = 1; + topo.forEach(elem => { + if (elem.name === 'null') { + elem.name = 'Daemon-' + String(ii++); + } + if (elem.ws_clients && elem.ws_clients instanceof Array) { + elem.ws_clients.forEach((ws: string) => { + if (ws_link[ws]) { + ws_link[ws].push(elem.name); + } else { + ws_link[ws] = [elem.name]; + } + }); + } + if (elem.ws_servers && elem.ws_servers instanceof Array) { + elem.ws_servers.forEach((ws: string) => { + if (ws_link[ws]) { + ws_link[ws].push(elem.name); + } else { + ws_link[ws] = [elem.name]; + } + }); + } + }); + + const nodes = {}; + this.links = []; + ii = 1; + topo.forEach(elem => { + let almostOne = false; + if (elem.ws_clients && elem.ws_clients.length) { + elem.ws_clients.forEach(wsCli => { + ws_link[wsCli].forEach(appName => { + if (appName !== elem.name) { + almostOne = true; + this.links.push({ source: elem.name, target: appName, type: 'ws-client' }); + } + }); + }); + } + if (elem.ws_servers && elem.ws_servers.length) { + elem.ws_servers.forEach(wsSvr => { + ws_link[wsSvr].forEach(appName => { + if (appName !== elem.name) { + almostOne = true; + this.links.push({ source: elem.name, target: appName, type: 'ws-server' }); + } + }); + }); + } + if (!almostOne) { + const name = '???-' + String(ii++); + this.links.push({ + source: elem.isServer ? name : elem.name, + target: elem.isServer ? elem.name : name, + type: 'not-connected', + }); + } + }); + + this.links.forEach(function (link) { + link.source = nodes[link.source] || (nodes[link.source] = { + name: link.source, + }); + link.target = nodes[link.target] || (nodes[link.target] = { + name: link.target, + }); + }); + + const width = this.document.getElementById('graph').clientWidth, + height = this.document.getElementById('graph').clientHeight; + + // Delete previous graph + if (this.svg) { + this.svg.remove(); + } + + // Create new graph + const force = d3.layout.force() + .nodes(d3.values(nodes)) + .links(this.links) + .size([width, height]) + .linkDistance(120) + .charge(-600) + .on('tick', tick) + .start(); + // const force = d3.forceSimulation() + + this.graph = d3.select('#graph'); + this.svg = this.graph.append('svg') + .attr('width', width) + .attr('height', height); + + // Define the div for the tooltip + /* + const divTooltip = d3.select('#graph').append('div') + .attr('class', 'tooltip') + .style('opacity', 0); + */ + + // Per-type markers, as they don't inherit styles. + this.svg.append('defs').selectAll('marker') + .data(['ws-server', 'ws-client', 'not-connected']) + .enter().append('marker') + .attr('id', function (d) { + return d; + }) + .attr('viewBox', '0 -5 10 10') + .attr('refX', 15) + .attr('refY', -1.5) + .attr('markerWidth', 12) + .attr('markerHeight', 12) + .attr('orient', 'auto') + .append('path') + .attr('d', 'M0,-5L10,0L0,5'); + + const path = this.svg.append('g').selectAll('path') + .data(force.links()) + .enter().append('path') + .attr('class', function (d) { + return 'link ' + d.type; + }) + .attr('marker-end', function (d) { + return 'url(#' + d.type + ')'; + }); + + const circle = this.svg.append('g').selectAll('circle') + .data(force.nodes()) + .enter().append('circle') + .attr('r', 12) + .call(force.drag); + + const text = this.svg.append('g').selectAll('text') + .data(force.nodes()) + .enter().append('text') + .attr('x', 20) + .attr('y', '.31em') + .text(function (d) { + return d.name; + }); + + /* TODO - SEB + circle.on('mouseover', d => { + divTooltip.transition() + .duration(200) + .style('opacity', .9); + divTooltip.html('This is a Tooltip <br/>' + d.close) + .style('left', (d3.event.pageX) + 'px') + .style('top', (d3.event.pageY - 28) + 'px'); + }); + + // Tooltip Object + const tooltip = d3.select('body') + .append('div').attr('id', 'tooltip') + .style('position', 'absolute') + .style('z-index', '10') + .style('visibility', 'hidden') + .text('a simple tooltip'); + */ + + // Use elliptical arc path segments to doubly-encode directionally. + function tick() { + path.attr('d', linkArc); + circle.attr('transform', transform); + text.attr('transform', transform); + } + + function linkArc(d) { + const dx = d.target.x - d.source.x, + dy = d.target.y - d.source.y, + dr = Math.sqrt(dx * dx + dy * dy); + return 'M' + d.source.x + ',' + d.source.y + 'A' + dr + ',' + dr + ' 0 0,1 ' + d.target.x + ',' + d.target.y; + } + + function transform(d) { + return 'translate(' + d.x + ',' + d.y + ')'; + } + } +} diff --git a/webapp/src/app/pages/supervision/supervision.component.html b/webapp/src/app/pages/supervision/supervision.component.html new file mode 100644 index 0000000..0db8ec8 --- /dev/null +++ b/webapp/src/app/pages/supervision/supervision.component.html @@ -0,0 +1,69 @@ +<!-- FIXME - cleanup +<div class="row" *ngIf="displayMode==='panels'"> + <div class="col-12"> + <nb-card-body> + <div class="col-9"> + <nb-actions size="small" *ngIf="displayMode==='panels'"> + <nb-action> + <button (click)="timeChange(-1)"> + <i class="nb-skip-backward"></i> + </button> + </nb-action> + <nb-action> + <button (click)="zoomOut()"> + <i class="nb-search"></i> + </button> + </nb-action> + <nb-action> + <button (click)="timeChange(1)"> + <i class="nb-skip-forward"></i> + </button> + </nb-action> + <nb-action> + <button disabled> + <pre> + start={{tm_from}} end={{tm_to}} + </pre> + </button> + </nb-action> + </nb-actions> + </div> + <div class="col-3 right"> + <nb-actions size="small"> + <nb-action> + <button (click)="displayModeChange()"> + <i class="fa fa-eye"></i> + </button> + </nb-action> + </nb-actions> + </div> + </nb-card-body> + </div> +</div> + +-- Display mode: using panels -- +<div *ngIf="displayMode==='panels'"> + <div class="row"> + <div class="col-md-6 col-lg-6"> + <iframe [src]="getPanel('req_evts_per_sec')" width="100%" height="320" frameborder="0"></iframe> + </div> + + <div class="col-md-6 col-lg-6"> + <iframe [src]="getPanel('evt_data_bytes')" width="100%" height="320" frameborder="0"></iframe> + </div> + </div> + + <div class="row"> + <div class="col-md-12"> + <iframe [src]="getPanel('table')" width="100%" height="500px" frameborder="0"></iframe> + </div> + </div> +</div> +--> + +<!-- Display mode: using dashboard --> +<div class="row" *ngIf="displayMode==='dashboard'"> + <div class="col-md-12"> + <iframe [src]="getDashboard('xds_supervisor')" width="100%" height="800px" frameborder="0"></iframe> + </div> +</div> diff --git a/webapp/src/app/pages/supervision/supervision.component.scss b/webapp/src/app/pages/supervision/supervision.component.scss new file mode 100644 index 0000000..a125e8d --- /dev/null +++ b/webapp/src/app/pages/supervision/supervision.component.scss @@ -0,0 +1,83 @@ +@import '../../@theme/styles/themes'; +@import '~@nebular/theme/components/card/card.component.theme'; +@import '~bootstrap/scss/mixins/breakpoints'; +@import '~@nebular/theme/styles/global/bootstrap/breakpoints'; +@include nb-install-component() { + nb-card-body { + display: flex; + align-items: center; + } + .action-groups-header { + flex-basis: 20%; + color: nb-theme(card-header-fg-heading); + font-family: nb-theme(card-header-font-family); + font-size: nb-theme(card-header-font-size); + font-weight: nb-theme(card-header-font-weight); + } + .nb-actions { + flex-basis: 80%; + } + .right { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + order: 1; + flex-direction: row-reverse; + } + nb-actions > nb-action { + padding: 0; + } + nb-action { + i { + color: nb-theme(color-fg); + font-size: 1.5rem; + margin-right: 1rem; + } + span { + font-family: nb-theme(font-secondary); + font-weight: nb-theme(font-weight-bold); + color: nb-theme(color-fg-heading); + text-transform: uppercase; + } + button { + margin: 0 auto; + padding: 0; + cursor: pointer; + border: none; + background: none; + display: flex; + align-items: center; + &:focus { + box-shadow: none; + outline: none; + } + } + } + @include media-breakpoint-down(md) { + nb-actions nb-action { + padding: 0 0.75rem; + } + } + @include media-breakpoint-down(sm) { + nb-card-body { + padding: 1rem; + } + nb-action { + font-size: 0.75rem; + i { + font-size: 2rem; + margin-right: 0.5rem; + } + } + } + @include media-breakpoint-down(is) { + nb-action i { + font-size: 1.75rem; + margin: 0; + } + span { + display: none; + } + } +} diff --git a/webapp/src/app/pages/supervision/supervision.component.ts b/webapp/src/app/pages/supervision/supervision.component.ts new file mode 100644 index 0000000..219f28f --- /dev/null +++ b/webapp/src/app/pages/supervision/supervision.component.ts @@ -0,0 +1,152 @@ +/** +* @license +* Copyright (C) 2017-2018 "IoT.bzh" +* Author Sebastien Douheret <sebastien@iot.bzh> +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import { Component, OnInit, Input } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import { Subject } from 'rxjs/Subject'; +import { NbThemeService } from '@nebular/theme'; +import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; + +import { SupervisionService } from '../../@core-xds/services/supervision.service'; +import { AlertService } from '../../@core-xds/services/alert.service'; + +export interface GrafanaDashboard { + name: string; + shortname: string; + url?: string; + safeUrl?: SafeResourceUrl; +} + +export interface GrafanaPanel { + name: string; + index: string; + url?: string; + safeUrl?: SafeResourceUrl; +} + +@Component({ + selector: 'xds-supervision', + styleUrls: ['./supervision.component.scss'], + templateUrl: './supervision.component.html', +}) + +export class SupervisionComponent implements OnInit { + + @Input() theme = 'light'; + @Input() tm_from = 1528988550450; + @Input() tm_to = 1528988842496; + @Input() scroll_factor = 10000; + @Input() zoom_factor = 100000; + + displayMode = 'dashboard'; + + private dashboards: Map<string, GrafanaDashboard> = new Map<string, GrafanaDashboard>([ + ['xds_supervisor', { name: 'AGL XDS Supervisor', shortname: 'agl-xds-supervisor' }], + ]); + + private panels: Map<string, GrafanaPanel> = new Map<string, GrafanaPanel>([ + ['table', { name: 'Supervisor traces table', index: '2' }], + ['evt_data_bytes', { name: 'Requests & Events per second', index: '5' }], + ['req_evts_per_sec', { name: 'Events Data bytes', index: '12' }], + ]); + + constructor( + private supervisionSvr: SupervisionService, + private alert: AlertService, + private themeService: NbThemeService, + private sanitizer: DomSanitizer, + ) { + } + + ngOnInit() { + this._initDashboard(); + this._initPanels(); + + this.themeService.onThemeChange().subscribe(tm => { + this.theme = (tm.name === 'cosmic') ? 'dark' : 'light'; + this.themeUpdate(); + }); + } + + getDashboard(name: string): SafeResourceUrl { + return this.dashboards.get(name).safeUrl; + } + + getPanel(name: string): SafeResourceUrl { + return this.panels.get(name).safeUrl; + } + + displayModeChange() { + if (this.displayMode === 'dashboard') { + this.displayMode = 'panels'; + } else { + this.displayMode = 'dashboard'; + } + } + + themeUpdate() { + this._initDashboard(); + this._initPanels(); + } + + timeChange(val: number) { + this.tm_from += val * this.scroll_factor; + this.tm_to += val * this.scroll_factor; + this._initPanels(); + } + + zoomOut() { + this.tm_from -= this.zoom_factor; + this.tm_to += this.zoom_factor; + this._initPanels(); + } + + + private _initDashboard() { + this.dashboards.forEach(dd => { + dd.url = this._buildDashboardUrl(dd.shortname, this.tm_from, this.tm_to, this.theme); + dd.safeUrl = this.sanitizer.bypassSecurityTrustResourceUrl(dd.url); + }); + } + private _initPanels() { + this.panels.forEach(gg => { + gg.url = this._buildPanelUrl(gg.index, this.tm_from, this.tm_to, this.theme); + gg.safeUrl = this.sanitizer.bypassSecurityTrustResourceUrl(gg.url); + }); + } + + private _buildDashboardUrl(sname: string, from: number, to: number, theme: string) { + let url = 'http://localhost:3000/d/Lbpwc6Iiz/' + sname; + url += '?orgId=1'; + url += '&from=' + from; + url += '&to=' + to; + url += '&theme=' + theme; + return url; + } + + private _buildPanelUrl(idx: string, from: number, to: number, theme: string) { + let url = 'http://localhost:3000/d-solo/Lbpwc6Iiz/agl-xds-supervisor'; + url += '?panelId=' + idx; + url += '&orgId=1'; + url += '&from=' + from; + url += '&to=' + to; + url += '&theme=' + theme; + url += '&sidemenu=close'; + return url; + } +} diff --git a/webapp/src/app/pages/supervision/supervision.module.ts b/webapp/src/app/pages/supervision/supervision.module.ts new file mode 100644 index 0000000..4c1cb0b --- /dev/null +++ b/webapp/src/app/pages/supervision/supervision.module.ts @@ -0,0 +1,37 @@ +/** +* @license +* Copyright (C) 2017-2018 "IoT.bzh" +* Author Sebastien Douheret <sebastien@iot.bzh> +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import { NgModule } from '@angular/core'; +import { ThemeModule } from '../../@theme/theme.module'; + +import { SupervisionComponent } from './supervision.component'; +import { SupervisionConfigComponent } from './supervision-config.component'; + + +@NgModule({ + imports: [ + ThemeModule, + ], + declarations: [ + SupervisionComponent, + SupervisionConfigComponent, + ], + entryComponents: [ + ], +}) +export class SupervisionModule { } diff --git a/webapp/src/app/pages/targets/target-add-modal/target-add-modal.component.ts b/webapp/src/app/pages/targets/target-add-modal/target-add-modal.component.ts index fdcb048..6260b87 100644 --- a/webapp/src/app/pages/targets/target-add-modal/target-add-modal.component.ts +++ b/webapp/src/app/pages/targets/target-add-modal/target-add-modal.component.ts @@ -88,7 +88,6 @@ export class TargetAddModalComponent implements OnInit { if (this._isIPstart(n)) { return 'Target_' + n; } -// SEB PB return n; }) .subscribe(value => { |