GUI2 implementation of device/flow/port/group/meter/host/link/tunnel view

Review comments incorporated.

Change-Id: I45dd6570961cc3e0f4ffddb7acbf02cd7d860de5
This commit is contained in:
Bhavesh 2018-07-19 16:29:18 +05:30 committed by Sean Condon
parent e910e406ba
commit 72ead49e48
101 changed files with 6324 additions and 366 deletions

View File

@ -4,4 +4,20 @@
fw/ contains framework related code
view/ contains view related code
view/ contains view related code
Device View - Shows the registered devices and navigates to/from port, flow, group, meter view
DeviceDetails view on device row selection
Added search option based on device id, name, etc.
Details panel view on row selection of port and flow view
Navigation to pipeconf view on device view isn't implemented yet
Editing friendly name into the details panel isn't implemented yet
Host View - Shows the hosts attached with the registered devices
HostDetails view on host row selection
Editing friendly name into the details panel isn't implemented yet
Link View - Shows the links between the devices
Tunnel View - Shows the tunnels created between two end-points

View File

@ -22,7 +22,7 @@ import { ActivatedRoute, Params } from '@angular/router';
import { VeilComponent } from './veil.component';
import { ConsoleLoggerService } from '../../../consolelogger.service';
import { FnService } from '../../../fw/util/fn.service';
import { FnService } from '../../util/fn.service';
import { LogService } from '../../../log.service';
import { KeyService } from '../../util/key.service';
import { GlyphService } from '../../svg/glyph.service';

View File

@ -16,13 +16,22 @@
<nav id="nav" [@navState]="ns.showNav">
<div class="nav-hdr">{{ lionFn('cat_platform') }}</div>
<a (click)="ns.hideNav()" routerLink="/apps" routerLinkActive="active">
<onos-icon iconId="nav_apps"></onos-icon>Apps</a>
<a (click)="ns.hideNav()" routerLink="/app" routerLinkActive="active">
<onos-icon iconId="nav_apps"></onos-icon> Apps</a>
<div class="nav-hdr">{{ lionFn('cat_network') }}</div>
<a (click)="ns.hideNav()" routerLink="/devices" routerLinkActive="active">
<onos-icon iconId="nav_devs"></onos-icon>Devices</a>
<a (click)="ns.hideNav()" routerLink="/device" routerLinkActive="active">
<onos-icon iconId="nav_devs"></onos-icon> Devices</a>
<a (click)="ns.hideNav()" routerLink="/link" routerLinkActive="active">
<onos-icon iconId="nav_links"></onos-icon> Links</a>
<a (click)="ns.hideNav()" routerLink="/host" routerLinkActive="active">
<onos-icon iconId="nav_hosts"></onos-icon> Hosts</a>
<a (click)="ns.hideNav()" routerLink="/tunnel" routerLinkActive="active">
<onos-icon iconId="nav_tunnels"></onos-icon> Tunnels</a>
<div class="nav-hdr">{{ lionFn('cat_other') }}</div>
</nav>

View File

@ -21,7 +21,7 @@ import { By } from '@angular/platform-browser';
import { of } from 'rxjs';
import { ConsoleLoggerService } from '../../../consolelogger.service';
import { FnService } from '../../../fw/util/fn.service';
import { FnService } from '../../util/fn.service';
import { IconComponent } from '../../svg/icon/icon.component';
import { IconService } from '../../svg/icon.service';
import { LionService } from '../../util/lion.service';
@ -127,6 +127,6 @@ describe('NavComponent', () => {
const appDe: DebugElement = fixture.debugElement;
const divDe = appDe.query(By.css('nav#nav a'));
const div: HTMLElement = divDe.nativeElement;
expect(div.textContent).toEqual('Apps');
expect(div.textContent).toEqual(' Apps');
});
});

View File

@ -14,10 +14,11 @@
* limitations under the License.
*/
import { Injectable } from '@angular/core';
import { FnService } from '../../fw/util/fn.service';
import { FnService } from '../util/fn.service';
import { LogService } from '../../log.service';
import * as gds from './glyphdata.service';
import * as d3 from 'd3';
import { SvgUtilService } from './svgutil.service';
// constants
const msgGS = 'GlyphService.';
@ -31,14 +32,25 @@ const rgs = 'registerGlyphSet(): ';
export class GlyphService {
// internal state
glyphs = d3.map();
api: Object;
constructor(
private fs: FnService,
// private gd: GlyphDataService,
private log: LogService
// private gd: GlyphDataService,
private log: LogService,
private sus: SvgUtilService
) {
this.clear();
this.init();
this.api = {
registerGlyphs: this.registerGlyphs,
registerGlyphSet: this.registerGlyphSet,
ids: this.ids,
glyph: this.glyph,
glyphDefined: this.glyphDefined,
loadDefs: this.loadDefs,
addGlyph: this.addGlyph,
};
this.log.debug('GlyphService constructed');
}
@ -121,7 +133,7 @@ export class GlyphService {
}
for (const [key, value] of data.entries()) {
// angular.forEach(data, function (value, key) {
// angular.forEach(data, function (value, key) {
if (key[0] !== '_') {
this.addToMap(key, value, vb, overwrite, dups);
}
@ -167,11 +179,28 @@ export class GlyphService {
}
}
defs.append('symbol')
.attr('id', g.id)
.attr('viewBox', g.vb)
.append('path')
.attr('d', g.d);
.attr('id', g.id)
.attr('viewBox', g.vb)
.append('path')
.attr('d', g.d);
}
});
}
addGlyph(elem: any, glyphId: string, size: number, overlay: any, trans: any) {
const sz = size || 40,
ovr = !!overlay,
xns = this.fs.isA(trans),
atr = {
width: sz,
height: sz,
'class': 'glyph',
'xlink:href': '#' + glyphId,
};
if (xns) {
atr.class = this.sus.translate(trans);
}
return elem.append('use').attr(atr).classed('overlay', ovr);
}
}

View File

@ -40,6 +40,8 @@ export const glyphMapping = new Map<string, string>([
['nonzero', 'nonzero'],
['close', 'xClose'],
['m_ports', 'm_ports'],
['topo', 'topo'],
['refresh', 'refresh'],

View File

@ -30,16 +30,58 @@ svg.embeddedIcon g.icon {
fill: none;
}
svg.embeddedIcon g.icon rect {
stroke: none;
fill: none;
.ctrl-btns div svg.embeddedIcon g.icon use {
fill: #e0dfd6;
}
svg.embeddedIcon g.icon .glyph {
stroke: none;
fill-rule: evenodd;
.ctrl-btns div.active svg.embeddedIcon g.icon use {
fill: #939598;
}
.ctrl-btns div.active:hover svg.embeddedIcon g.icon use {
fill: #ce5b58;
}
svg.embeddedIcon .icon.appInactive .glyph {
fill: none !important;
.ctrl-btns div.current-view svg.embeddedIcon g.icon rect {
fill: #518ecc;
}
.ctrl-btns div.current-view svg.embeddedIcon g.icon use {
fill: white;
}
svg.embeddedIcon .icon.active .glyph {
fill: #04bf34;
}
svg.embeddedIcon .icon.inactive .glyph {
fill: #c0242b;
}
svg.embeddedIcon .icon.active-rect .glyph {
fill:#939598;
}
svg.embeddedIcon .icon.active-sort .glyph {
fill:#333333;
}
svg.embeddedIcon g.icon.active-rect:hover use {
fill: #ce5b58;
}
svg.embeddedIcon g.icon.active-type .glyph {
fill: #3c3a3a;
}
svg.embeddedIcon g.icon.active-close:hover use {
fill: #ce5b58;
}
svg.embeddedIcon g.icon.active-close .glyph {
fill: #333333;
}
svg.embeddedIcon g.icon.details-icon .glyph {
fill: #0071bd;;
}

View File

@ -13,6 +13,7 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<div class="tooltip">
<svg class="embeddedIcon" [attr.width]="iconSize" [attr.height]="iconSize" viewBox="0 0 50 50" (mouseover)="toolTipDisp = toolTip" (mouseout)="toolTipDisp = undefined">
<g class="icon" [ngClass]="classes">
<rect width="50" height="50" rx="5"></rect>
@ -20,4 +21,8 @@
</g>
</svg>
<!-- I'm fixing class as light as view encapsulation changes how the hirerarchy of css is handled -->
<p id="tooltip" class="light" *ngIf="toolTip" [ngStyle]="{ 'display': toolTipDisp ? 'inline-block':'none' }">{{ toolTipDisp }}</p>
<!-- <p id="tooltip" class="light" *ngIf="toolTip" [ngStyle]="{ 'display': toolTipDisp ? 'inline-block':'none'}">{{ toolTipDisp }}</p> -->
<span class="tooltiptext" [ngStyle]="{ 'display': toolTipDisp ? 'inline-block':'none'}">{{toolTipDisp}}</span>
</div>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -18,28 +18,15 @@
ONOS GUI -- Icon Service (theme) -- CSS file
*/
.light div.close-btn svg.embeddedIcon g.icon .glyph {
div.close-btn svg.embeddedIcon g.icon .glyph {
fill: #333333;
}
/* Sortable table headers */
.light div.tableColSort svg.embeddedIcon .icon .glyph {
div.tableColSort svg.embeddedIcon .icon .glyph {
fill: #353333;
}
/* active / inactive (check/xmark) icons */
.light svg.embeddedIcon .icon.active .glyph {
fill: #04bf34;
}
.light svg.embeddedIcon .icon.inactive .glyph {
fill: #c0242b;
}
.light table svg.embeddedIcon .icon .glyph {
fill: #3c3a3a;
}
/* --- Control Buttons --- */
/* INACTIVE */
@ -50,9 +37,10 @@ svg.embeddedIcon g.icon use {
/* ACTIVE */
svg.embeddedIcon g.icon.active use {
.ctrl-btns div.active svg.embeddedIcon g.icon use {
fill: #939598;
}
svg.embeddedIcon g.icon.active:hover use {
fill: #ce5b58;
}
@ -82,25 +70,25 @@ svg.embeddedIcon g.icon.refresh.active:hover use {
/* ========== DARK Theme ========== */
.dark div.close-btn svg.embeddedIcon g.icon .glyph {
div.close-btn svg.embeddedIcon g.icon .glyph {
fill: #8d8d8d;
}
.dark div.tableColSort svg.embeddedIcon .icon .glyph {
div.tableColSort svg.embeddedIcon .icon .glyph {
fill: #888888;
}
.dark svg.embeddedIcon .icon.active .glyph {
/*svg.embeddedIcon .icon.active .glyph {
fill: #04bf34;
}
.dark svg.embeddedIcon .icon.inactive .glyph {
svg.embeddedIcon .icon.inactive .glyph {
fill: #c0242b;
}
}*/
.dark table svg.embeddedIcon .icon .glyph {
table svg.embeddedIcon .icon .glyph {
fill: #9999aa;
}
/*
svg.embeddedIcon g.icon .glyph {
fill: #007dc4;
@ -109,4 +97,12 @@ svg.embeddedIcon g.icon .glyph {
svg.embeddedIcon:hover g.icon .glyph {
fill: #20b2ff;
}
*/
*/
svg.embeddedIcon g.icon.devIcon_SWITCH .glyph {
fill: #0071bd;;
}
svg.embeddedIcon g.icon.hostIcon_endstation .glyph {
fill: #0071bd;;
}

View File

@ -25,6 +25,37 @@
padding: 5px;
position: absolute;
z-index: 5000;
display: none;
display: inline-block;
pointer-events: none;
top: 40px;
right: auto;
/* width: 240px; */
}
.tooltip {
position: relative;
display: inline-block;
}
.tooltip .tooltiptext {
display: inline-block;
visibility: hidden;
background-color: #dbeffc;
color: #3c3a3a;
border-color: #c7c7c0;
text-align: center;
border-radius: 6px;
font-size: 80%;
padding: 5px;
/* Position the tooltip */
position: absolute;
z-index: 5000;
top: 42px;
right: 10%;
white-space: nowrap;
}
.tooltip:hover .tooltiptext {
visibility: visible;
}

View File

@ -23,7 +23,7 @@ import { LogService } from '../../log.service';
* The SVG Util Service provides a miscellany of utility functions.
*/
@Injectable({
providedIn: 'root',
providedIn: 'root',
})
export class SvgUtilService {

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
import { Injectable } from '@angular/core';
import { FnService } from '../util/fn.service';
import { FnService } from './fn.service';
import { LogService } from '../../log.service';
/**

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
import { Injectable } from '@angular/core';
import { FnService } from '../util/fn.service';
import { FnService } from './fn.service';
import { LogService } from '../../log.service';
import { WebSocketService } from '../remote/websocket.service';
@ -22,16 +22,96 @@ import { WebSocketService } from '../remote/websocket.service';
* ONOS GUI -- Util -- User Preference Service
*/
@Injectable({
providedIn: 'root',
providedIn: 'root',
})
export class PrefsService {
protected Prefs;
protected handlers: string[] = [];
cache: any;
listeners: any;
constructor(
private fs: FnService,
private log: LogService,
private wss: WebSocketService
protected fs: FnService,
protected log: LogService,
protected wss: WebSocketService
) {
this.cache = {};
this.wss.bindHandlers(new Map<string, (data) => void>([
[this.Prefs, (data) => this.updatePrefs(data)]
]));
this.handlers.push(this.Prefs);
this.log.debug('PrefsService constructed');
}
setPrefs(name: string, obj: any) {
// keep a cached copy of the object and send an update to server
this.cache[name] = obj;
this.wss.sendEvent('updatePrefReq', { key: name, value: obj });
}
updatePrefs(data: any) {
this.cache = data;
this.listeners.forEach(function (lsnr) { lsnr(); });
}
asNumbers(obj: any, keys?: any, not?: any) {
if (!obj) {
return null;
}
const skip = {};
if (not) {
keys.forEach(k => {
skip[k] = 1;
}
);
}
if (!keys || not) {
// do them all
Array.from(obj).forEach((v, k) => {
if (!not || !skip[k]) {
obj[k] = Number(obj[k]);
}
});
} else {
// do the explicitly named keys
keys.forEach(k => {
obj[k] = Number(obj[k]);
});
}
return obj;
}
getPrefs(name: string, defaults: any, qparams?: string) {
const obj = Object.assign({}, defaults || {}, this.cache[name] || {});
// if query params are specified, they override...
if (this.fs.isO(qparams)) {
obj.forEach(k => {
if (qparams.hasOwnProperty(k)) {
obj[k] = qparams[k];
}
});
}
return obj;
}
// merge preferences:
// The assumption here is that obj is a sparse object, and that the
// defined keys should overwrite the corresponding values, but any
// existing keys that are NOT explicitly defined here should be left
// alone (not deleted).
mergePrefs(name: string, obj: any) {
const merged = this.cache[name] || {};
this.setPrefs(name, Object.assign(merged, obj));
}
addListener(listener: any) {
this.listeners.push(listener);
}
removeListener(listener: any) {
this.listeners = this.listeners.filter(function (obj) { return obj === listener; });
}
}

View File

@ -14,7 +14,6 @@
* limitations under the License.
*/
import { Injectable } from '@angular/core';
import { FnService } from '../util/fn.service';
import { LogService } from '../../log.service';
/**
@ -26,7 +25,6 @@ import { LogService } from '../../log.service';
export class RandomService {
constructor(
private fs: FnService,
private log: LogService
) {
this.log.debug('RandomService constructed');

View File

@ -19,6 +19,7 @@ import { LogService } from '../../log.service';
import { WebSocketService } from '../remote/websocket.service';
import { PanelBaseImpl } from './panel.base';
import { Output, EventEmitter, Input } from '@angular/core';
/**
* A generic model of the data returned from the *DetailsResponse
@ -37,6 +38,9 @@ interface DetailsResponse {
*/
export abstract class DetailsPanelBaseImpl extends PanelBaseImpl {
@Input() id: string;
@Output() closeEvent = new EventEmitter<string>();
private root: string;
private req: string;
private resp: string;
@ -85,16 +89,10 @@ export abstract class DetailsPanelBaseImpl extends PanelBaseImpl {
}
/**
* Details Panel Data Request - should be called whenever id changes
* If id is empty, no request is made
* Details Panel Data Request - should be called whenever row id changes
*/
requestDetailsPanelData(id: string) {
if (id === '') {
return;
}
requestDetailsPanelData(query: any) {
this.closed = false;
const query = {'id': id};
// Do not send if the Web Socket hasn't opened
if (this.wss.isConnected()) {
if (this.fs.debugOn('panel')) {
@ -109,5 +107,8 @@ export abstract class DetailsPanelBaseImpl extends PanelBaseImpl {
*/
close(): void {
this.closed = true;
this.id = null;
this.closeEvent.emit(this.id);
}
}

View File

@ -22,9 +22,9 @@
position: absolute;
z-index: 100;
display: block;
top: 120px;
width: 500px;
right: -505px;
top: 160px;
width: 544px;
right: -550px;
opacity: 100;
padding: 2px;

View File

@ -67,6 +67,11 @@ export interface SortParams {
secondDir: SortDir;
}
export interface PayloadParams {
devId: string;
}
/**
* ONOS GUI -- Widget -- Table Base class
*/
@ -74,7 +79,7 @@ export abstract class TableBaseImpl {
// attributes from the interface
protected annots: TableAnnots;
protected changedData: string[] = [];
protected payloadParams: any;
protected payloadParams: PayloadParams;
protected sortParams: SortParams;
protected selectCallback; // Function
protected parentSelCb = null;
@ -164,7 +169,7 @@ export abstract class TableBaseImpl {
if (JSON.stringify(newTableData) !== JSON.stringify(this.tableData)) {
this.log.debug('table data has changed');
const oldTableData: any[] = this.tableData;
this.tableData = [ ...newTableData ]; // ES6 spread syntax
this.tableData = [...newTableData]; // ES6 spread syntax
// only flash the row if the data already exists
if (oldTableData.length > 0) {
for (const idx in newTableData) {
@ -282,4 +287,12 @@ export abstract class TableBaseImpl {
return '';
}
}
/**
* De-selects the row
*/
deselectRow(event) {
this.log.debug('Details panel close event');
this.selId = event;
}
}

View File

@ -31,14 +31,13 @@ div.summary-list table {
div.summary-list div.table-body {
overflow-y: scroll;
max-height:70vh;
}
div.summary-list div.table-body::-webkit-scrollbar {
display: none;
}
div.summary-list tr.no-data td {
div.summary-list div.table-body tr.no-data td {
text-align: center;
font-style: italic;
}

View File

@ -13,9 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Directive, ElementRef } from '@angular/core';
import { AfterContentChecked, Directive, ElementRef, Inject } from '@angular/core';
import { FnService } from '../util/fn.service';
import { LogService } from '../../log.service';
import { MastService } from '../mast/mast.service';
import { HostListener } from '@angular/core';
import * as d3 from 'd3';
/**
* ONOS GUI -- Widget -- Table Resize Directive
@ -23,20 +26,58 @@ import { LogService } from '../../log.service';
@Directive({
selector: '[onosTableResize]',
})
export class TableResizeDirective {
export class TableResizeDirective implements AfterContentChecked {
constructor(
private fs: FnService,
public log: LogService,
private el: ElementRef,
) {
pdg = 22;
tables: any;
this.windowSize();
this.log.debug('TableResizeDirective constructed');
constructor(protected fs: FnService,
protected log: LogService,
protected mast: MastService,
protected el: ElementRef,
@Inject('Window') private w: Window) {
log.info('TableResizeDirective constructed');
}
windowSize() {
ngAfterContentChecked() {
this.tables = {
thead: d3.select('div.table-header').select('table'),
tbody: d3.select('div.table-body').select('table')
};
this.windowSize(this.tables);
}
windowSize(tables: any) {
const wsz = this.fs.windowSize(0, 30);
this.el.nativeElement.style.width = wsz.width + 'px';
this.adjustTable(tables, wsz.width, wsz.height);
}
@HostListener('window:resize', ['event'])
onResize(event: any) {
this.windowSize(this.tables);
return {
h: this.w.innerHeight,
w: this.w.innerWidth
};
}
adjustTable(tables: any, width: number, height: number) {
this._width(tables.thead, width + 'px');
this._width(tables.tbody, width + 'px');
this.setHeight(tables.thead, d3.select('div.table-body'), height);
}
_width(elem, width) {
elem.style('width', width);
}
setHeight(thead: any, body: any, height: number) {
const h = height - (this.mast.mastHeight +
this.fs.noPxStyle(d3.select('.tabular-header'), 'height') +
this.fs.noPxStyle(thead, 'height') + this.pdg);
body.style('height', h + 'px');
}
}

View File

@ -22,16 +22,44 @@ import { Routes, RouterModule } from '@angular/router';
*/
const onosRoutes: Routes = [
{
path: 'apps',
path: 'app',
loadChildren: 'app/view/apps/apps.module#AppsModule'
},
{
path: 'devices',
path: 'device',
loadChildren: 'app/view/device/device.module#DeviceModule'
},
{
path: 'link',
loadChildren: 'app/view/link/link.module#LinkModule'
},
{
path: 'host',
loadChildren: 'app/view/host/host.module#HostModule'
},
{
path: 'tunnel',
loadChildren: 'app/view/tunnel/tunnel.module#TunnelModule'
},
{
path: 'flow',
loadChildren: 'app/view/flow/flow.module#FlowModule'
},
{
path: 'port',
loadChildren: 'app/view/port/port.module#PortModule'
},
{
path: 'group',
loadChildren: 'app/view/group/group.module#GroupModule'
},
{
path: 'meter',
loadChildren: 'app/view/meter/meter.module#MeterModule'
},
{
path: '',
redirectTo: 'devices', // Default to devices view - change to topo in future
redirectTo: 'device', // Default to devices view - change to topo in future
pathMatch: 'full'
}
];

View File

@ -1,5 +1,5 @@
/*
* Copyright 2015-present Open Networking Foundation
* Copyright 2018-present Open Networking Foundation
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.

View File

@ -1,5 +1,5 @@
/*
* Copyright 2015-present Open Networking Foundation
* Copyright 2018-present Open Networking Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@ -36,24 +36,24 @@
<div class="separator"></div>
<div class="active" (click)="triggerForm()">
<onos-icon classes="{{ 'active upload' }}"
<onos-icon classes="{{ 'active-rect upload' }}"
iconId="upload" iconSize="42" toolTip="{{ uploadTip }}"></onos-icon>
</div>
<div (click)="confirmAction(AppActionEnum.ACTIVATE)">
<onos-icon classes="{{ ctrlBtnState.installed?'active play':'play' }}"
<div (click)="(!!selId) ? confirmAction(AppActionEnum.ACTIVATE) : ''">
<onos-icon classes="{{ ctrlBtnState.installed?'active-rect play':'play' }}"
iconId="play" iconSize="42" toolTip="{{ activateTip }}"></onos-icon>
</div>
<div (click)="confirmAction(AppActionEnum.DEACTIVATE)">
<onos-icon classes="{{ ctrlBtnState.active?'active stop':'stop' }}"
<div (click)="(!!selId) ? confirmAction(AppActionEnum.DEACTIVATE) : ''">
<onos-icon classes="{{ ctrlBtnState.active?'active-rect stop':'stop' }}"
iconId="stop" iconSize="42" toolTip="{{ deactivateTip }}"></onos-icon>
</div>
<div (click)="confirmAction(AppActionEnum.UNINSTALL)">
<onos-icon classes="{{ ctrlBtnState.selection?'active garbage':'garbage' }}"
<div (click)="(!!selId) ? confirmAction(AppActionEnum.UNINSTALL) : ''">
<onos-icon classes="{{ ctrlBtnState.selection?'active-rect garbage':'garbage' }}"
iconId="garbage" iconSize="42" toolTip="{{ uninstallTip }}"></onos-icon>
</div>
<div (click)="downloadApp()">
<onos-icon classes="{{ ctrlBtnState.selection?'active download':'download' }}"
<div (click)="(!!selId) ? downloadApp() : ''">
<onos-icon classes="{{ ctrlBtnState.selection?'active-rect download':'download' }}"
iconId="download" iconSize="42" toolTip="{{ downloadTip }}"></onos-icon>
</div>
</div>
@ -74,34 +74,34 @@
</div>
<div id="summary-list" class="summary-list">
<div id="summary-list" class="summary-list" onosTableResize>
<div class="table-header">
<table onosTableResize>
<table>
<tr>
<th colId="state" [ngStyle]="{width: '32px'}" class="table-icon" (click)="onSort('state')">
<onos-icon classes="active" [iconId]="sortIcon('state')"></onos-icon>
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('state')"></onos-icon>
</th>
<th colId="icon" [ngStyle]="{width: '32px'}" class="table-icon"></th>
<th colId="title" (click)="onSort('title')">{{lionFn('title')}}
<onos-icon classes="active" [iconId]="sortIcon('title')"></onos-icon>
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('title')"></onos-icon>
</th>
<th colId="id" (click)="onSort('id')">{{lionFn('app_id')}}
<onos-icon classes="active" [iconId]="sortIcon('id')"></onos-icon>
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('id')"></onos-icon>
</th>
<th colId="version" (click)="onSort('version')"> {{lionFn('version')}}
<onos-icon classes="active" [iconId]="sortIcon('version')"></onos-icon>
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('version')"></onos-icon>
</th>
<th colId="category" (click)="onSort('category')"> {{lionFn('category')}}
<onos-icon classes="active" [iconId]="sortIcon('category')"></onos-icon>
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('category')"></onos-icon>
</th>
<th colId="origin" (click)="onSort('origin')"> {{lionFn('origin')}}
<onos-icon classes="active" [iconId]="sortIcon('origin')"></onos-icon>
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('origin')"></onos-icon>
</th>
</tr>
</table>
</div>
<div class="table-body">
<table onosTableResize>
<table>
<tr *ngIf="tableData.length === 0" class="no-data">
<td colspan="5">
{{annots.no_rows_msg}}
@ -137,6 +137,6 @@
The advantage in 2) is that panel can be animated in and out, as it is not
killed every time the selection changes.
-->
<onos-appsdetails class="floatpanels" id="{{ selId }}"></onos-appsdetails>
<onos-appsdetails class="floatpanels" id="{{ selId }}" (closeEvent)="deselectRow($event)"></onos-appsdetails>
</div>

View File

@ -38,6 +38,7 @@ import { TableFilterPipe } from '../../../fw/widget/tablefilter.pipe';
import { UrlFnService } from '../../../fw/remote/urlfn.service';
import { WebSocketService } from '../../../fw/remote/websocket.service';
import { of } from 'rxjs';
import { } from 'jasmine';
class MockActivatedRoute extends ActivatedRoute {
constructor(params: Params) {
@ -46,33 +47,33 @@ class MockActivatedRoute extends ActivatedRoute {
}
}
class MockDialogService {}
class MockDialogService { }
class MockFnService {}
class MockFnService { }
class MockHttpClient {}
class MockIconService {
loadIconDef() {}
loadIconDef() { }
}
class MockKeyService {}
class MockKeyService { }
class MockLoadingService {
startAnim() {}
stop() {}
waiting() {}
startAnim() { }
stop() { }
waiting() { }
}
class MockThemeService {}
class MockThemeService { }
class MockUrlFnService {}
class MockUrlFnService { }
class MockWebSocketService {
createWebSocket() {}
createWebSocket() { }
isConnected() { return false; }
unbindHandlers() {}
bindHandlers() {}
unbindHandlers() { }
bindHandlers() { }
}
/**
@ -90,21 +91,21 @@ describe('AppsComponent', () => {
test: 'test1'
}
};
const mockLion = (key) => {
const mockLion = (key) => {
return bundleObj[key] || '%' + key + '%';
};
beforeEach(async(() => {
const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
ar = new MockActivatedRoute({'debug': 'txrx'});
ar = new MockActivatedRoute({ 'debug': 'txrx' });
windowMock = <any>{
location: <any> {
location: <any>{
hostname: 'foo',
host: 'foo',
port: '80',
protocol: 'http',
search: { debug: 'true'},
search: { debug: 'true' },
href: 'ws://foo:123/onos/ui/websock/path',
absUrl: 'ws://foo:123/onos/ui/websock/path'
}
@ -127,7 +128,8 @@ describe('AppsComponent', () => {
{ provide: HttpClient, useClass: MockHttpClient },
{ provide: IconService, useClass: MockIconService },
{ provide: KeyService, useClass: MockKeyService },
{ provide: LionService, useFactory: (() => {
{
provide: LionService, useFactory: (() => {
return {
bundle: ((bundleId) => mockLion),
ubercache: new Array(),
@ -143,7 +145,7 @@ describe('AppsComponent', () => {
{ provide: 'Window', useValue: windowMock },
]
})
.compileComponents();
.compileComponents();
logServiceSpy = TestBed.get(LogService);
}));

View File

@ -337,4 +337,13 @@ export class AppsComponent extends TableBaseImpl implements OnInit, OnDestroy {
evt.preventDefault();
evt.stopPropagation();
}
deselectRow(event) {
this.log.debug('Details panel close event');
this.selId = event;
this.ctrlBtnState = <CtrlBtnState>{
installed: undefined,
active: undefined
};
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2016-present Open Networking Foundation
* Copyright 2018-present Open Networking Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -41,7 +41,6 @@ div.dropping {
#ov-app div.summary-list .table-body {
overflow:scroll;
max-height:70vh;
}
#ov-app h2 {
display: inline-block;

View File

@ -1,5 +1,5 @@
/*
* Copyright 2015-present Open Networking Foundation
* Copyright 2018-present Open Networking Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@ -1,5 +1,5 @@
<!--
~ Copyright 2014-present Open Networking Foundation
~ Copyright 2018-present Open Networking Foundation
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.

View File

@ -21,10 +21,10 @@ import { By } from '@angular/platform-browser';
import { LogService } from '../../../log.service';
import { AppsDetailsComponent } from './appsdetails.component';
import { FnService } from '../../../../app/fw/util/fn.service';
import { IconComponent } from '../../../../app/fw/svg/icon/icon.component';
import { IconService } from '../../../../app/fw/svg/icon.service';
import { LionService } from '../../../../app/fw/util/lion.service';
import { FnService } from '../../../fw/util/fn.service';
import { IconComponent } from '../../../fw/svg/icon/icon.component';
import { IconService } from '../../../fw/svg/icon.service';
import { LionService } from '../../../fw/util/lion.service';
import { UrlFnService } from '../../../fw/remote/urlfn.service';
import { WebSocketService } from '../../../fw/remote/websocket.service';
import { of } from 'rxjs';

View File

@ -101,8 +101,20 @@ export class AppsDetailsComponent extends DetailsPanelBaseImpl implements OnInit
this.log.debug('App Details Component destroyed');
}
/**
* Details Panel Data Request on row selection changes
* Should be called whenever id changes
* If id is empty, no request is made
*/
ngOnChanges() {
this.requestDetailsPanelData(this.id);
if (this.id === '') {
return '';
} else {
const query = {
'id': this.id
};
this.requestDetailsPanelData(query);
}
}
iconUrl(appId: string): string {

View File

@ -15,8 +15,7 @@
*/
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { DeviceComponent } from './device.component';
import { DeviceComponent } from './device/device.component';
const deviceRoutes: Routes = [
{

View File

@ -1,114 +0,0 @@
<!--
~ Copyright 2014-present Open Networking Foundation
~
~ 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.
-->
<div id="ov-device">
<div class="tabular-header">
<h2>Devices ({{ tableData.length }} total)</h2>
<div class="ctrl-btns">
<div class="refresh" (click)="toggleRefresh()">
<!-- See icon.theme.css for the defintions of the classes active and refresh-->
<onos-icon classes="{{ autoRefresh?'active refresh':'refresh' }}"
iconId="refresh" iconSize="42" toolTip="{{ autoRefreshTip }}"></onos-icon>
</div>
<div class="separator"></div>
<div>
<onos-icon classes="{{ selId ? 'current-view':undefined }}"
iconId="deviceTable" iconSize="42"></onos-icon>
</div>
<div routerLink="/flow" routerLinkActive="active">
<onos-icon classes="{{ selId ? 'active':undefined }}"
iconId="flowTable" iconSize="42" toolTip="{{ flowTip }}"></onos-icon>
</div>
<div routerLink="/port" routerLinkActive="active">
<onos-icon classes="{{ selId ? 'active':undefined }}"
iconId="portTable" iconSize="42" toolTip="{{ portTip }}"></onos-icon>
</div>
<div routerLink="/group" routerLinkActive="active">
<onos-icon classes="{{ selId ? 'active':undefined }}"
iconId="groupTable" iconSize="42" toolTip="{{ groupTip }}"></onos-icon>
</div>
<div routerLink="/meter" routerLinkActive="active">
<onos-icon classes="{{ selId ? 'active':undefined }}"
iconId="meterTable" iconSize="42" toolTip="{{ meterTip }}"></onos-icon>
</div>
<div routerLink="/pipeconf" routerLinkActive="active">
<onos-icon classes="{{ selId ? 'active':undefined }}"
iconId="pipeconfTable" iconSize="42" toolTip="{{ pipeconfTip }}"></onos-icon>
</div>
</div>
</div>
<div class="summary-list" onos-table-resize>
<table onos-flash-changes id-prop="id" width="100%">
<tr class="table-header">
<th colId="available" class="table-icon" sortable></th>
<th colId="type" class="table-icon"></th>
<th colId="name" sortable>Friendly Name </th>
<th colId="id" sortable>Device ID </th>
<th colId="masterid" [ngClass]="{width: '130px'}" sortable>Master </th>
<th colId="num_ports" [ngClass]="{width: '70px'}" sortable>Ports </th>
<th colId="mfr" sortable>Vendor </th>
<th colId="hw" sortable>H/W Version </th>
<th colId="sw" sortable>S/W Version </th>
<th colId="protocol" [ngClass]="{width: '100px'}" sortable>Protocol </th>
</tr>
<tr class="table-body" *ngIf="tableData.length === 0" class="no-data">
<td colspan="9">{{ annots.noRowsMsg }}</td>
</tr>
<tr class="table-body" *ngFor="let dev of tableData"
(click)="selectCallback($event, dev)"
[ngClass]="{selected: dev.id === selId, 'data-change': isChanged(dev.id)}">
<td class="table-icon">
<!--[ngClass]="{width: devAvail.getBBox().width}"-->
<onos-icon iconId="{{dev._iconid_available}}"></onos-icon>
</td>
<td class="table-icon">
<onos-icon iconId="{{dev._iconid_type}}"></onos-icon>
</td>
<td>{{ dev.name }}</td>
<td>{{ dev.id }}</td>
<td>{{ dev.masterid }}</td>
<td>{{ dev.num_ports }}</td>
<td>{{ dev.mfr }}</td>
<td>{{ dev.hw }}</td>
<td>{{ dev.sw }}</td>
<td>{{ dev.protocol }}</td>
</tr>
</table>
</div>
<small>
<p>TODO (21 Jun 18): Add in:</p>
<ul>
<li>Scrolling for long lists of devices</li>
<li>Sorting by column</li>
<li>Left align header columns</li>
<li>Move tooltip to underneath icon</li>
<li>Correct width and icon colour of active and device icon columns</li>
<li>Add device details panel</li>
<li>Add more unit tests</li>
<li>Make icon for #undefined work (e.g. for device type olt or unknown)</li>
<li>Change loading service to fade in and out and have a threshold of </li>
</ul>
</small>
</div>

View File

@ -1,5 +1,5 @@
/*
* Copyright 2015-present Open Networking Foundation
* Copyright 2018-present Open Networking Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,22 +16,26 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DeviceRoutingModule } from './device-routing.module';
import { DeviceComponent } from './device.component';
import { DeviceDetailsPanelDirective } from './devicedetailspanel.directive';
import { DeviceComponent } from './device/device.component';
import { SvgModule } from '../../fw/svg/svg.module';
import { WidgetModule } from '../../fw/widget/widget.module';
import { FormsModule } from '@angular/forms';
import { DeviceDetailsComponent } from './devicedetails/devicedetails.component';
/**
* ONOS GUI -- Device View Module
*/
@NgModule({
imports: [
CommonModule,
DeviceRoutingModule,
SvgModule
],
declarations: [
DeviceComponent,
DeviceDetailsPanelDirective
]
imports: [
CommonModule,
DeviceRoutingModule,
SvgModule,
WidgetModule,
FormsModule
],
declarations: [
DeviceComponent,
DeviceDetailsComponent
]
})
export class DeviceModule { }

View File

@ -1,5 +1,5 @@
/*
* Copyright 2015-present Open Networking Foundation
* Copyright 2018-present Open Networking Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@ -0,0 +1,121 @@
<!--
~ Copyright 2018-present Open Networking Foundation
~
~ 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.
-->
<div id="ov-device">
<div class="tabular-header">
<h2>Devices ({{ tableData.length }} total)</h2>
<div class="ctrl-btns">
<div class="refresh" (click)="toggleRefresh()">
<!-- See icon.theme.css for the defintions of the classes active and refresh-->
<onos-icon classes="{{ autoRefresh?'active refresh':'refresh' }}" iconId="refresh" iconSize="42" toolTip="{{ autoRefreshTip }}"></onos-icon>
</div>
<div class="separator"></div>
<div>
<onos-icon classes="{{ selId ? 'current-view':undefined }}" iconId="deviceTable" iconSize="42"></onos-icon>
</div>
<div (click)="navto('/flow')">
<onos-icon classes="{{ selId ? 'active-rect' :undefined}}" iconId="flowTable" iconSize="42" toolTip="{{ flowTip }}"></onos-icon>
</div>
<div (click)="navto('/port')">
<onos-icon classes="{{ selId ? 'active-rect' :undefined}}" iconId="portTable" iconSize="42" toolTip="{{ portTip }}"></onos-icon>
</div>
<div (click)="navto('/group')">
<onos-icon classes="{{ selId ? 'active-rect' :undefined}}" iconId="groupTable" iconSize="42" toolTip="{{ groupTip }}"></onos-icon>
</div>
<div (click)="navto('/meter')">
<onos-icon classes="{{ selId ? 'active-rect' :undefined}}" iconId="meterTable" iconSize="42" toolTip="{{ meterTip }}"></onos-icon>
</div>
<div (click)="navto('/pipeconf')">
<onos-icon classes="{{ selId ? 'active-rect' :undefined}}" iconId="pipeconfTable" iconSize="42" toolTip="{{ pipeconfTip }}"></onos-icon>
</div>
</div>
<div class="search">
<input id="searchinput" [(ngModel)]="tableDataFilter.queryStr" type="search" #search placeholder="Search" />
<select [(ngModel)]="tableDataFilter.queryBy">
<option value="" disabled>Search By</option>
<option value="$">All Fields</option>
<option value="id">Device-Id</option>
<option value="name">Name</option>
<option value="protocol">Protocol</option>
</select>
</div>
</div>
<div id="summary-list" class="summary-list" onosTableResize>
<div class="table-header">
<table>
<tr>
<td colId="available" class="table-icon"></td>
<td colId="type" class="table-icon"></td>
<td colId="name" (click)="onSort('name')">Friendly Name
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('name')"></onos-icon>
</td>
<td colId="id" (click)="onSort('id')">Device ID
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('id')"></onos-icon>
</td>
<td colId="masterid" [ngClass]="{width: '130px'}" (click)="onSort('masterid')">Master
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('masterid')"></onos-icon>
</td>
<td colId="num_ports" [ngClass]="{width: '70px'}" (click)="onSort('num_ports')">Ports
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('num_ports')"></onos-icon>
</td>
<td colId="mfr" (click)="onSort('mfr')">Vendor
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('mfr')"></onos-icon>
</td>
<td colId="hw" (click)="onSort('hw')">H/W Version
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('hw')"></onos-icon>
</td>
<td colId="sw" (click)="onSort('sw')">S/W Version
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('sw')"></onos-icon>
</td>
<td colId="protocol" [ngClass]="{width: '100px'}" (click)="onSort('protocol')">Protocol
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('protocol')"></onos-icon>
</td>
</tr>
</table>
</div>
<div class="table-body">
<table>
<tr class="table-body" *ngIf="tableData.length === 0" class="no-data">
<td colspan="9">{{ annots.noRowsMsg }}</td>
</tr>
<tr *ngFor="let dev of tableData | filter : tableDataFilter" (click)="selectCallback($event, dev)" [ngClass]="{selected: dev.id === selId, 'data-change': isChanged(dev.id)}">
<td class="table-icon">
<onos-icon classes="{{ dev._iconid_available}}" iconId={{dev._iconid_available}}></onos-icon>
</td>
<td class="table-icon">
<onos-icon classes="{{dev._iconid_type? 'active-type':undefined}}" iconId="{{dev._iconid_type}}"></onos-icon>
</td>
<td>{{ dev.name }}</td>
<td>{{ dev.id }}</td>
<td>{{ dev.masterid }}</td>
<td>{{ dev.num_ports }}</td>
<td>{{ dev.mfr }}</td>
<td>{{ dev.hw }}</td>
<td>{{ dev.sw }}</td>
<td>{{ dev.protocol }}</td>
</tr>
</table>
</div>
</div>
<onos-devicedetails class="floatpanels" id="{{ selId }}" (closeEvent)="deselectRow($event)"></onos-devicedetails>
</div>

View File

@ -1,5 +1,5 @@
/*
* Copyright 2015-present Open Networking Foundation
* Copyright 2018-present Open Networking Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -17,21 +17,26 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ActivatedRoute, Params } from '@angular/router';
import { DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';
import { LogService } from '../../log.service';
import { LogService } from '../../../log.service';
import { DeviceComponent } from './device.component';
import { } from 'jasmine';
import { FnService, WindowSize } from '../../fw/util/fn.service';
import { IconService } from '../../fw/svg/icon.service';
import { GlyphService } from '../../fw/svg/glyph.service';
import { IconComponent } from '../../fw/svg/icon/icon.component';
import { KeyService } from '../../fw/util/key.service';
import { LoadingService } from '../../fw/layer/loading.service';
import { NavService } from '../../fw/nav/nav.service';
import { MastService } from '../../fw/mast/mast.service';
import { SvgUtilService } from '../../fw/svg/svgutil.service';
import { ThemeService } from '../../fw/util/theme.service';
import { WebSocketService } from '../../fw/remote/websocket.service';
import { FnService } from '../../../fw/util/fn.service';
import { IconService } from '../../../fw/svg/icon.service';
import { GlyphService } from '../../../fw/svg/glyph.service';
import { IconComponent } from '../../../fw/svg/icon/icon.component';
import { KeyService } from '../../../fw/util/key.service';
import { LoadingService } from '../../../fw/layer/loading.service';
import { NavService } from '../../../fw/nav/nav.service';
import { MastService } from '../../../fw/mast/mast.service';
import { TableFilterPipe } from '../../../fw/widget/tablefilter.pipe';
import { ThemeService } from '../../../fw/util/theme.service';
import { WebSocketService } from '../../../fw/remote/websocket.service';
import { of } from 'rxjs';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FormsModule } from '@angular/forms';
import { DeviceDetailsComponent } from './../devicedetails/devicedetails.component';
import { RouterTestingModule } from '@angular/router/testing';
class MockActivatedRoute extends ActivatedRoute {
constructor(params: Params) {
@ -40,38 +45,30 @@ class MockActivatedRoute extends ActivatedRoute {
}
}
class MockDetailsPanelService {}
class MockFnService {}
class MockIconService {
loadIconDef() {}
loadIconDef() { }
}
class MockGlyphService {}
class MockGlyphService { }
class MockKeyService {}
class MockKeyService { }
class MockLoadingService {
startAnim() {}
stop() {}
startAnim() { }
stop() { }
}
class MockNavService {}
class MockNavService { }
class MockMastService {}
class MockMastService { }
class MockTableBuilderService {}
class MockTableDetailService {}
class MockThemeService {}
class MockThemeService { }
class MockWebSocketService {
createWebSocket() {}
createWebSocket() { }
isConnected() { return false; }
unbindHandlers() {}
bindHandlers() {}
unbindHandlers() { }
bindHandlers() { }
}
/**
@ -87,15 +84,15 @@ describe('DeviceComponent', () => {
beforeEach(async(() => {
const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
ar = new MockActivatedRoute({'debug': 'txrx'});
ar = new MockActivatedRoute({ 'debug': 'txrx' });
windowMock = <any>{
location: <any> {
location: <any>{
hostname: 'foo',
host: 'foo',
port: '80',
protocol: 'http',
search: { debug: 'true'},
search: { debug: 'true' },
href: 'ws://foo:123/onos/ui/websock/path',
absUrl: 'ws://foo:123/onos/ui/websock/path'
}
@ -103,7 +100,8 @@ describe('DeviceComponent', () => {
fs = new FnService(ar, logSpy, windowMock);
TestBed.configureTestingModule({
declarations: [ DeviceComponent, IconComponent ],
imports: [BrowserAnimationsModule, FormsModule, RouterTestingModule],
declarations: [DeviceComponent, IconComponent, TableFilterPipe, DeviceDetailsComponent],
providers: [
{ provide: FnService, useValue: fs },
{ provide: IconService, useClass: MockIconService },
@ -116,9 +114,8 @@ describe('DeviceComponent', () => {
{ provide: ThemeService, useClass: MockThemeService },
{ provide: WebSocketService, useClass: MockWebSocketService },
{ provide: 'Window', useValue: windowMock },
]
})
.compileComponents();
]
}).compileComponents();
logServiceSpy = TestBed.get(LogService);
}));
@ -132,10 +129,29 @@ describe('DeviceComponent', () => {
expect(component).toBeTruthy();
});
it('should have a div.tabular-header inside a div#ov-device', () => {
const devDe: DebugElement = fixture.debugElement;
const divDe = devDe.query(By.css('div#ov-device div.tabular-header'));
expect(divDe).toBeTruthy();
});
it('should have .table-header with "Friendly Name..."', () => {
const appDe: DebugElement = fixture.debugElement;
const divDe = appDe.query(By.css('.table-header'));
const devDe: DebugElement = fixture.debugElement;
const divDe = devDe.query(By.css('div#ov-device div.table-header'));
const div: HTMLElement = divDe.nativeElement;
expect(div.textContent).toEqual('Friendly Name Device ID Master Ports Vendor H/W Version S/W Version Protocol ');
});
it('should have a refresh button inside the div.tabular-header', () => {
const devDe: DebugElement = fixture.debugElement;
const divDe = devDe.query(By.css('div#ov-device div.tabular-header div.ctrl-btns div.refresh'));
expect(divDe).toBeTruthy();
});
it('should have a div.table-body ', () => {
const devDe: DebugElement = fixture.debugElement;
const divDe = devDe.query(By.css('div#ov-device div.table-body'));
expect(divDe).toBeTruthy();
});
});

View File

@ -1,5 +1,5 @@
/*
* Copyright 2015-present Open Networking Foundation
* Copyright 2018-present Open Networking Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -13,16 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, OnInit, OnDestroy, Inject } from '@angular/core';
import { FnService } from '../../fw/util/fn.service';
import { IconService } from '../../fw/svg/icon.service';
import { KeyService } from '../../fw/util/key.service';
import { LoadingService } from '../../fw/layer/loading.service';
import { LogService } from '../../log.service';
import { MastService } from '../../fw/mast/mast.service';
import { NavService } from '../../fw/nav/nav.service';
import { TableBaseImpl, TableResponse } from '../../fw/widget/table.base';
import { WebSocketService } from '../../fw/remote/websocket.service';
import { Component, OnInit, OnDestroy} from '@angular/core';
import { FnService } from '../../../fw/util/fn.service';
import { LoadingService } from '../../../fw/layer/loading.service';
import { LogService } from '../../../log.service';
import { TableBaseImpl, TableResponse, SortDir } from '../../../fw/widget/table.base';
import { WebSocketService } from '../../../fw/remote/websocket.service';
import { ActivatedRoute, Router } from '@angular/router';
/**
* Model of the response from WebSocket
@ -55,9 +52,9 @@ interface Device {
* ONOS GUI -- Device View Component
*/
@Component({
selector: 'onos-device',
templateUrl: './device.component.html',
styleUrls: ['./device.component.css', './device.theme.css', '../../fw/widget/table.css', '../../fw/widget/table.theme.css']
selector: 'onos-device',
templateUrl: './device.component.html',
styleUrls: ['./device.component.css', './device.theme.css', '../../../fw/widget/table.css', '../../../fw/widget/table.theme.css']
})
export class DeviceComponent extends TableBaseImpl implements OnInit, OnDestroy {
@ -71,16 +68,29 @@ export class DeviceComponent extends TableBaseImpl implements OnInit, OnDestroy
constructor(
protected fs: FnService,
protected ls: LoadingService,
private is: IconService,
private ks: KeyService,
protected log: LogService,
private mast: MastService,
private nav: NavService,
protected as: ActivatedRoute,
protected router: Router,
protected wss: WebSocketService,
@Inject('Window') private window: Window,
) {
super(fs, ls, log, wss, 'device');
this.responseCallback = this.deviceResponseCb;
this.as.queryParams.subscribe(params => {
this.selId = params['devId'];
});
this.payloadParams = {
devId: this.selId
};
this.sortParams = {
firstCol: 'name',
firstDir: SortDir.asc,
secondCol: 'id',
secondDir: SortDir.desc,
};
}
ngOnInit() {
@ -97,4 +107,11 @@ export class DeviceComponent extends TableBaseImpl implements OnInit, OnDestroy
this.log.debug('Device response received for ', data.devices.length, 'devices');
}
navto(path) {
this.log.debug('navigate');
if (this.selId) {
this.router.navigate([path], { queryParams: { devId: this.selId } });
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2016-present Open Networking Foundation
* Copyright 2018-present Open Networking Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -28,4 +28,33 @@
.light #device-details-panel .bottom tr:nth-child(even) {
background-color: #f4f4f4;
}
#ov-device .tabular-header {
text-align: left;
}
#ov-device div.summary-list .table-header td {
font-weight: bold;
font-variant: small-caps;
text-transform: uppercase;
font-size: 10pt;
padding-top: 8px;
padding-bottom: 8px;
letter-spacing: 0.02em;
cursor: pointer;
background-color: #e5e5e6;
color: #3c3a3a;
}
#ov-device div.summary-list .table-body {
overflow:scroll;
}
#ov-device h2 {
display: inline-block;
}
#ov-device, div.ctrl-btns {
}
#ov-device th, td {
text-align: left;
padding: 8px;
}

View File

@ -0,0 +1,114 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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.
*/
#device-details-panel.floatpanel {
z-index: 0;
font-size: 10pt;
top: 185px;
}
#device-details-panel.floatpanel a {
font-weight: bold;
}
#device-details-panel .container {
padding: 0 30px;
}
#device-details-panel .close-btn {
position: absolute;
right:5px;
top: 5px;
cursor: pointer;
}
#device-details-panel .dev-icon {
display: inline-block;
padding: 0 6px 0 0;
vertical-align: middle;
}
#device-details-panel h2 {
display: inline-block;
margin: 8px 0;
font-weight: bold;
font-size: 16pt;
}
#device-details-panel h2 input {
font-size: 0.90em;
width: 106%;
}
#device-details-panel .actionBtns div {
padding: 12px 6px;
}
#device-details-panel hr {
width: 100%;
margin: 2px auto;
}
#device-details-panel .top-tables {
font-size: 10pt;
white-space: nowrap;
}
#device-details-panel td.label {
font-weight: bold;
text-align: right;
padding-right: 6px;
}
#device-details-panel .bottom table {
border-spacing: 0;
height: 358px;
width: 520px;
overflow: auto;
display: block;
}
#device-details-panel .bottom th {
letter-spacing: 0.02em;
}
#device-details-panel .bottom th,
#device-details-panel .bottom td {
padding: 6px 12px;
text-align: center;
}
#device-details-panel .top div.left {
float: left;
text-align: left;
padding: 0 10px 0 0;
}
#device-details-panel .top div.right {
display: inline-block;
}
#device-details-panel .editable {
border-bottom: 1px dashed #ca504b;
}
#device-details-panel .clickable {
cursor: pointer;
}
#device-details-panel .bottom thead tr {
background-color: #e5e5e6;
}

View File

@ -0,0 +1,111 @@
<!--
~ Copyright 2018-present Open Networking Foundation
~
~ 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.
-->
<div id="device-details-panel" class="floatpanel" [@deviceDetailsState]="id!=='' && !closed">
<div class="container">
<div class="top">
<div class="close-btn">
<onos-icon class="close-btn" classes="active-close" iconId="close" iconSize="20" (click)="close()"></onos-icon>
</div>
<div class="dev-icon">
<onos-icon classes="{{detailsData._iconid_type? 'details-icon':undefined}}" iconId="{{detailsData._iconid_type}}" [iconSize]="40"></onos-icon>
</div>
<h2 class="editable clickable">{{detailsData.id}}</h2>
<div class="top-content">
<div class="top-tables">
<div class="left">
<table>
<tbody>
<tr>
<td class="label" width="110">URI :</td>
<td class="value" width="80">{{detailsData.id}}</td>
</tr>
<tr>
<td class="label" width="110">Type :</td>
<td class="value" width="80">{{detailsData.type}}</td>
</tr>
<tr>
<td class="label" width="110">Master ID :</td>
<td class="value" width="80">{{detailsData.masterid}}</td>
</tr>
<tr>
<td class="label" width="110">Chassis ID :</td>
<td class="value" width="80">{{detailsData.chassisid}}</td>
</tr>
<tr>
<td class="label" width="110">Vendor :</td>
<td class="value" width="80">{{detailsData.mfr}}</td>
</tr>
</tbody>
</table>
</div>
<div class="right">
<table>
<tbody>
<tr>
<td class="label" width="110">H/W Version :</td>
<td class="value" width="80">{{detailsData.hw}}</td>
</tr>
<tr>
<td class="label" width="110">S/W Version :</td>
<td class="value" width="80">{{detailsData.sw}}</td>
</tr>
<tr>
<td class="label" width="110">Protocol :</td>
<td class="value" width="80">{{detailsData.protocol}}</td>
</tr>
<tr>
<td class="label" width="110">Serial # :</td>
<td class="value" width="80">{{detailsData.serial}}</td>
</tr>
<tr>
<td class="label" width="110">Pipeconf :</td>
<td class="value" width="80">{{detailsData.pipeconf}}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<hr>
</div>
<div class="bottom">
<h2 class="ports-title">Ports</h2>
<table>
<thead>
<tr>
<th>Enabled</th>
<th>ID</th>
<th>Speed</th>
<th>Type</th>
<th>Egress Links</th>
<th>Name</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let port of detailsData.ports">
<td>{{port.enabled}}</td>
<td>{{port.id}}</td>
<td>{{port.speed}}</td>
<td>{{port.type}}</td>
<td>{{port.elinks_dest}}</td>
<td>{{port.name}}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>

View File

@ -0,0 +1,142 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DeviceDetailsComponent } from './devicedetails.component';
import { ActivatedRoute, Params } from '@angular/router';
import { of } from 'rxjs/index';
import { FnService } from '../../../fw/util/fn.service';
import { LogService } from '../../../log.service';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { IconService } from '../../../fw/svg/icon.service';
import { WebSocketService } from '../../../fw/remote/websocket.service';
import { DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';
import { } from 'jasmine';
import { IconComponent } from '../../../fw/svg/icon/icon.component';
class MockActivatedRoute extends ActivatedRoute {
constructor(params: Params) {
super();
this.queryParams = of(params);
}
}
class MockIconService {
classes = 'active-close';
loadIconDef() { }
}
class MockWebSocketService {
createWebSocket() { }
isConnected() { return false; }
unbindHandlers() { }
bindHandlers() { }
}
describe('DeviceDetailsComponent', () => {
let fs: FnService;
let ar: MockActivatedRoute;
let windowMock: Window;
let logServiceSpy: jasmine.SpyObj<LogService>;
let component: DeviceDetailsComponent;
let fixture: ComponentFixture<DeviceDetailsComponent>;
beforeEach(async(() => {
const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
ar = new MockActivatedRoute({ 'debug': 'panel' });
windowMock = <any>{
location: <any>{
hostname: 'foo',
host: 'foo',
port: '80',
protocol: 'http',
search: { debug: 'true' },
href: 'ws://foo:123/onos/ui/websock/path',
absUrl: 'ws://foo:123/onos/ui/websock/path'
}
};
fs = new FnService(ar, logSpy, windowMock);
TestBed.configureTestingModule({
imports: [BrowserAnimationsModule],
declarations: [DeviceDetailsComponent, IconComponent],
providers: [
{ provide: FnService, useValue: fs },
{ provide: IconService, useClass: MockIconService },
{ provide: LogService, useValue: logSpy },
{ provide: WebSocketService, useClass: MockWebSocketService },
{ provide: 'Window', useValue: windowMock },
]
}).compileComponents();
logServiceSpy = TestBed.get(LogService);
}));
beforeEach(() => {
fixture = TestBed.createComponent(DeviceDetailsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should have an div.close-btn div.top inside a div.container', () => {
const devDe: DebugElement = fixture.debugElement;
const divDe = devDe.query(By.css('div.container div.top div.close-btn'));
expect(divDe).toBeTruthy();
});
it('should have a div.dev-icon inside a div.top inside a div.container', () => {
const devDe: DebugElement = fixture.debugElement;
const divDe = devDe.query(By.css('div.container div.top div.dev-icon'));
const div: HTMLElement = divDe.nativeElement;
expect(div.textContent).toEqual('');
});
it('should have a div.top-content inside a div.top inside a div.container', () => {
const devDe: DebugElement = fixture.debugElement;
const divDe = devDe.query(By.css('div.container div.top div.top-content'));
expect(divDe).toBeTruthy();
});
it('should have a dev.left inside a div.top-tables inside a div.top-content', () => {
const devDe: DebugElement = fixture.debugElement;
const divDe = devDe.query(By.css('div.top-content div.top-tables div.left'));
const div: HTMLElement = divDe.nativeElement;
expect(div.textContent).toEqual('URI :Type :Master ID :Chassis ID :Vendor :');
});
it('should have a dev.right inside a div.top-tables inside a div.top-content', () => {
const devDe: DebugElement = fixture.debugElement;
const divDe = devDe.query(By.css('div.top-content div.top-tables div.right'));
const div: HTMLElement = divDe.nativeElement;
expect(div.textContent).toEqual('H/W Version :S/W Version :Protocol :Serial # :Pipeconf :');
});
it('should have a div.bottom inside a div.container', () => {
const devDe: DebugElement = fixture.debugElement;
const divDe = devDe.query(By.css('div.container div.bottom'));
expect(divDe).toBeTruthy();
});
it('should have a h2.ports-title inside a div.bottom inside a div.container', () => {
const devDe: DebugElement = fixture.debugElement;
const divDe = devDe.query(By.css('div.container div.bottom h2.ports-title'));
expect(divDe).toBeTruthy();
});
});

View File

@ -0,0 +1,103 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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, Input, OnInit, OnDestroy, OnChanges } from '@angular/core';
import { trigger, state, style, animate, transition } from '@angular/animations';
import { FnService } from '../../../fw/util/fn.service';
import { LoadingService } from '../../../fw/layer/loading.service';
import { LogService } from '../../../log.service';
import { WebSocketService } from '../../../fw/remote/websocket.service';
import { DetailsPanelBaseImpl } from '../../../fw/widget/detailspanel.base';
import { IconService } from '../../../fw/svg/icon.service';
/**
* The details view when a device row is clicked from the Device view
*
* This is expected to be passed an 'id' and it makes a call
* to the WebSocket with an deviceDetailsRequest, and gets back an
* deviceDetailsResponse.
*
* The animated fly-in is controlled by the animation below
* The deviceDetailsState is attached to device-details-panel
* and is false (flies out) when id='' and true (flies in) when
* id has a value
*/
@Component({
selector: 'onos-devicedetails',
templateUrl: './devicedetails.component.html',
styleUrls: ['./devicedetails.component.css',
'../../../fw/widget/panel.css', '../../../fw/widget/panel-theme.css'
],
animations: [
trigger('deviceDetailsState', [
state('true', style({
transform: 'translateX(-100%)',
opacity: '100'
})),
state('false', style({
transform: 'translateX(0%)',
opacity: '0'
})),
transition('0 => 1', animate('100ms ease-in')),
transition('1 => 0', animate('100ms ease-out'))
])
]
})
export class DeviceDetailsComponent extends DetailsPanelBaseImpl implements OnInit, OnDestroy, OnChanges {
@Input() id: string;
constructor(protected fs: FnService,
protected ls: LoadingService,
protected log: LogService,
protected is: IconService,
protected wss: WebSocketService
) {
super(fs, ls, log, wss, 'device');
}
ngOnInit() {
this.init();
this.log.debug('App Details Component initialized:', this.id);
}
/**
* Stop listening to appDetailsResponse on WebSocket
*/
ngOnDestroy() {
this.destroy();
this.log.debug('App Details Component destroyed');
}
/**
* Details Panel Data Request on row selection changes
* Should be called whenever id changes
* If id is empty, no request is made
*/
ngOnChanges() {
if (this.id === '') {
return '';
} else {
const query = {
'id': this.id
};
this.requestDetailsPanelData(query);
}
}
}

View File

@ -1,39 +0,0 @@
/*
* Copyright 2015-present Open Networking Foundation
*
* 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 { Directive, Inject } from '@angular/core';
import { KeyService } from '../../fw/util/key.service';
import { LogService } from '../../log.service';
/**
* ONOS GUI -- Device Details Panel Directive
*
* TODO: figure out if this should be a directive or a component. In the old
* code it was a directive, but was referred to in device.html like a component
* would be
*/
@Directive({
selector: '[onosDeviceDetailsPanel]'
})
export class DeviceDetailsPanelDirective {
constructor(
private ks: KeyService,
private log: LogService
) {
this.log.debug('DeviceDetailsPanelDirective constructed');
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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 { Routes, RouterModule } from '@angular/router';
import { FlowComponent } from './flow/flow.component';
const flowRoutes: Routes = [
{
path: '',
component: FlowComponent
}
];
/**
* ONOS GUI -- Flows Tabular View Feature Routing Module - allows it to be lazy loaded
*
* See https://angular.io/guide/lazy-loading-ngmodules
*/
@NgModule({
imports: [RouterModule.forChild(flowRoutes)],
exports: [RouterModule]
})
export class FlowRoutingModule { }

View File

@ -0,0 +1,41 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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 { CommonModule } from '@angular/common';
import { FlowComponent } from './flow/flow.component';
import { SvgModule } from '../../fw/svg/svg.module';
import { WidgetModule } from '../../fw/widget/widget.module';
import { FlowRoutingModule } from './flow-routing.module';
import { FormsModule } from '@angular/forms';
import { FlowDetailsComponent } from './flowdetails/flowdetails/flowdetails.component';
/**
* ONOS GUI -- Flow View Module
*/
@NgModule({
imports: [
CommonModule,
SvgModule,
FlowRoutingModule,
FormsModule,
WidgetModule
],
declarations: [
FlowComponent,
FlowDetailsComponent
]
})
export class FlowModule { }

View File

@ -0,0 +1,102 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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.
*/
/*
ONOS GUI -- Flow View (layout) -- CSS file
*/
#ov-flow h2 {
display: inline-block;
}
#ov-flow div.ctrl-btns {
}
#ov-flow td {
text-align: center;
}
#ov-flow td.right {
text-align: right;
}
#ov-flow td.selector,
#ov-flow td.treatment {
text-align: left;
padding-left: 36px;
}
#ov-flow .tabular-header {
text-align: left;
}
#ov-flow div.summary-list .table-header td {
font-weight: bold;
font-variant: small-caps;
text-transform: uppercase;
font-size: 10pt;
padding-top: 8px;
padding-bottom: 8px;
letter-spacing: 0.02em;
cursor: pointer;
background-color: #e5e5e6;
color: #3c3a3a;
}
/* More in generic panel.css */
#flow-details-panel.floatpanel {
z-index: 0;
}
#flow-details-panel .container {
padding: 8px 12px;
}
#flow-details-panel .close-btn {
position: absolute;
right: 12px;
top: 12px;
cursor: pointer;
}
#flow-details-panel .dev-icon {
display: inline-block;
padding: 0 6px 0 0;
vertical-align: middle;
}
#flow-details-panel h2 {
display: inline-block;
margin: 8px 0;
font-size: 16pt;
font-weight: lighter;
}
#flow-details-panel h3 {
display: inline-block;
margin: 8px 0;
font-size: 11pt;
font-variant: small-caps;
text-transform: uppercase;
}
#flow-details-panel .top-content table {
font-size: 10pt;
}
#flow-details-panel td.label {
font-weight: bold;
text-align: right;
padding-right: 6px;
}

View File

@ -0,0 +1,136 @@
<!--
~ Copyright 2018-present Open Networking Foundation
~
~ 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.
-->
<div id="ov-flow" xmlns="http://www.w3.org/1999/html">
<div class="tabular-header">
<h2>
{{lionFn('title_flows')}} {{id}} ({{ tableData.length }} {{ lionFn('total') }})
</h2>
<div class="ctrl-btns">
<div class="refresh" (click)="toggleRefresh()">
<!-- See icon.theme.css for the defintions of the classes active and refresh-->
<onos-icon classes="{{ autoRefresh?'active refresh':'refresh' }}" iconId="refresh" iconSize="42" toolTip="{{ autoRefreshTip }}"></onos-icon>
</div>
<div class="separator"></div>
<span *ngIf="brief" (click)="briefToggle()">
<div>
<onos-icon classes="{{ id ? 'active-rect' :undefined}}" iconId="plus" iconSize="42" toolTip="{{detailTip}}"></onos-icon>
</div>
</span>
<span *ngIf="!brief" (click)="briefToggle()">
<div>
<onos-icon classes="{{ id ? 'active-rect' :undefined}}" iconId="minus" iconSize="42" toolTip="{{briefTip}}"></onos-icon>
</div>
</span>
<div class="separator"></div>
<div routerLink="/device" [queryParams]="{ devId: id }" routerLinkActive="active">
<onos-icon classes="{{ id ? 'active-rect':undefined }}" iconId="deviceTable" iconSize="42" toolTip="{{deviceTip}}"></onos-icon>
</div>
<div>
<onos-icon classes="{{ id ? 'current-view' :undefined}}" iconId="flowTable" iconSize="42"></onos-icon>
</div>
<div routerLink="/port" [queryParams]="{ devId: id }" routerLinkActive="active">
<onos-icon classes="{{ id ? 'active-rect' :undefined}}" iconId="portTable" iconSize="42" toolTip="{{ portTip }}"></onos-icon>
</div>
<div routerLink="/group" [queryParams]="{ devId: id }" routerLinkActive="active">
<onos-icon classes="{{ id ? 'active-rect' :undefined}}" iconId="groupTable" iconSize="42" toolTip="{{ groupTip }}"></onos-icon>
</div>
<div routerLink="/meter" [queryParams]="{ devId: id }" routerLinkActive="active">
<onos-icon classes="{{ id ? 'active-rect' :undefined}}" iconId="meterTable" iconSize="42" toolTip="{{ meterTip }}"></onos-icon>
</div>
<div routerLink="/pipeconf" [queryParams]="{ devId: id }" routerLinkActive="active">
<onos-icon classes="{{ id ? 'active-rect' :undefined}}" iconId="pipeconfTable" iconSize="42" toolTip="{{ pipeconfTip }}"></onos-icon>
</div>
</div>
<div class="search">
<input id="searchinput" [(ngModel)]="tableDataFilter.queryStr" type="search" #search placeholder="Search" />
<select [(ngModel)]="tableDataFilter.queryBy">
<option value="" disabled>Search By</option>
<option value="$">All Fields</option>
<option value="priority">{{lionFn('priority')}}</option>
<option value="tableName">{{lionFn('tableName')}}</option>
<option value="selector">{{lionFn('selector')}}</option>
<option value="treatment">{{lionFn('treatment')}}</option>
<option value="appName">{{lionFn('appName')}}</option>
</select>
</div>
</div>
<div class="summary-list" onosTableResize>
<div class="table-header">
<table>
<tr>
<td colId="state" (click)="onSort('state')">{{lionFn('state')}}
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('state')"></onos-icon>
</td>
<td colId="packets" (click)="onSort('packets')">{{lionFn('packets')}}
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('packets')"></onos-icon>
</td>
<td colId="duration" (click)="onSort('duration')">{{lionFn('duration')}}
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('duration')"></onos-icon>
</td>
<td colId="priority" (click)="onSort('priority')">{{lionFn('priority')}}
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('priority')"></onos-icon>
</td>
<td colId="tableName" (click)="onSort('tableName')">{{lionFn('tableName')}}
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('tableName')"></onos-icon>
</td>
<td colId="selector" (click)="onSort('selector')">{{lionFn('selector')}}
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('selector')"></onos-icon>
</td>
<td colId="treatment" (click)="onSort('treatment')">{{lionFn('treatment')}}
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('treatment')"></onos-icon>
</td>
<td colId="appName" (click)="onSort('appName')">{{lionFn('appName')}}
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('appName')"></onos-icon>
</td>
</tr>
</table>
</div>
<div class="table-body">
<table>
<tr class="table-body" *ngIf="tableData.length === 0" class="no-data">
<td colspan="9">{{annots.noRowsMsg}}</td>
</tr>
<ng-template ngFor let-flow [ngForOf]="tableData | filter : tableDataFilter">
<tr (click)="selectCallback($event, flow)" [ngClass]="{selected: flow.id === selId, 'data-change': isChanged(flow.id)}">
<td>{{flow.state}}</td>
<td>{{flow.packets}}</td>
<td>{{flow.duration}}</td>
<td>{{flow.priority}}</td>
<td>{{flow.tableName}}</td>
<td>{{flow.selector_c}}</td>
<td>{{flow.treatment_c}}</td>
<td>{{flow.appName}}</td>
</tr>
<tr (click)="selectCallback($event, flow)" [ngClass]="{selected: flow.id === selId, 'data-change': isChanged(flow.id)}" [hidden]="brief">
<td class="selector" colspan="8">{{flow.selector}} </td>
</tr>
<tr (click)="selectCallback($event, flow)" [ngClass]="{selected: flow.id === selId, 'data-change': isChanged(flow.id)}" [hidden]="brief">
<td class="treatment" colspan="8">{{flow.treatment}}</td>
</tr>
</ng-template>
</table>
</div>
</div>
<onos-flowdetails class="floatpanels" flowId="{{ selId }}" appId="{{ selRowAppId }}" (closeEvent)="deselectRow($event)"></onos-flowdetails>
</div>

View File

@ -0,0 +1,183 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FlowComponent } from './flow.component';
import { ActivatedRoute, Params } from '@angular/router';
import { of } from 'rxjs/index';
import { LogService } from '../../../log.service';
import { FnService } from '../../../fw/util/fn.service';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FormsModule } from '@angular/forms';
import { RouterTestingModule } from '@angular/router/testing';
import { TableFilterPipe } from '../../../fw/widget/tablefilter.pipe';
import { IconComponent } from '../../../fw/svg/icon/icon.component';
import { IconService } from '../../../fw/svg/icon.service';
import { GlyphService } from '../../../fw/svg/glyph.service';
import { KeyService } from '../../../fw/util/key.service';
import { LoadingService } from '../../../fw/layer/loading.service';
import { MastService } from '../../../fw/mast/mast.service';
import { NavService } from '../../../fw/nav/nav.service';
import { ThemeService } from '../../../fw/util/theme.service';
import { WebSocketService } from '../../../fw/remote/websocket.service';
import { DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';
import { LionService } from '../../../fw/util/lion.service';
import { FlowDetailsComponent } from '../flowdetails/flowdetails/flowdetails.component';
class MockActivatedRoute extends ActivatedRoute {
constructor(params: Params) {
super();
this.queryParams = of(params);
}
}
class MockIconService {
loadIconDef() { }
}
class MockGlyphService { }
class MockKeyService { }
class MockLoadingService {
startAnim() { }
stop() { }
}
class MockNavService { }
class MockMastService { }
class MockThemeService { }
class MockWebSocketService {
createWebSocket() { }
isConnected() { return false; }
unbindHandlers() { }
bindHandlers() { }
}
/**
* ONOS GUI -- Flow View Module - Unit Tests
*/
describe('FlowComponent', () => {
let fs: FnService;
let ar: MockActivatedRoute;
let windowMock: Window;
let logServiceSpy: jasmine.SpyObj<LogService>;
let component: FlowComponent;
let fixture: ComponentFixture<FlowComponent>;
const bundleObj = {
'core.view.Flow': {
test: 'test1'
}
};
const mockLion = (key) => {
return bundleObj[key] || '%' + key + '%';
};
beforeEach(async(() => {
const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
ar = new MockActivatedRoute({ 'debug': 'txrx' });
windowMock = <any>{
location: <any>{
hostname: 'foo',
host: 'foo',
port: '80',
protocol: 'http',
search: { debug: 'true' },
href: 'ws://foo:123/onos/ui/websock/path',
absUrl: 'ws://foo:123/onos/ui/websock/path'
}
};
fs = new FnService(ar, logSpy, windowMock);
TestBed.configureTestingModule({
imports: [BrowserAnimationsModule, FormsModule, RouterTestingModule],
declarations: [FlowComponent, IconComponent, TableFilterPipe, FlowDetailsComponent],
providers: [
{ provide: FnService, useValue: fs },
{ provide: IconService, useClass: MockIconService },
{ provide: GlyphService, useClass: MockGlyphService },
{ provide: KeyService, useClass: MockKeyService },
{
provide: LionService, useFactory: (() => {
return {
bundle: ((bundleId) => mockLion),
ubercache: new Array(),
loadCbs: new Map<string, () => void>([])
};
})
},
{ provide: LoadingService, useClass: MockLoadingService },
{ provide: MastService, useClass: MockMastService },
{ provide: NavService, useClass: MockNavService },
{ provide: LogService, useValue: logSpy },
{ provide: ThemeService, useClass: MockThemeService },
{ provide: WebSocketService, useClass: MockWebSocketService },
{ provide: 'Window', useValue: windowMock },
]
}).compileComponents();
logServiceSpy = TestBed.get(LogService);
}));
beforeEach(() => {
fixture = TestBed.createComponent(FlowComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should have a div.tabular-header inside a div#ov-flow', () => {
const flowDe: DebugElement = fixture.debugElement;
const divDe = flowDe.query(By.css('div#ov-flow div.tabular-header'));
expect(divDe).toBeTruthy();
});
it('should have a h2 inside the div.tabular-header', () => {
const flowDe: DebugElement = fixture.debugElement;
const divDe = flowDe.query(By.css('div#ov-flow div.tabular-header h2'));
const div: HTMLElement = divDe.nativeElement;
expect(div.textContent).toEqual(' %title_flows% (0 %total%) ');
});
it('should have .table-header with "State..."', () => {
const flowDe: DebugElement = fixture.debugElement;
const divDe = flowDe.query(By.css('div#ov-flow div.table-header'));
const div: HTMLElement = divDe.nativeElement;
expect(div.textContent).toEqual('%state% %packets% %duration% %priority% %tableName% %selector% %treatment% %appName% ');
});
it('should have a refresh button inside the div.tabular-header', () => {
const flowDe: DebugElement = fixture.debugElement;
const divDe = flowDe.query(By.css('div#ov-flow div.tabular-header div.ctrl-btns div.refresh'));
expect(divDe).toBeTruthy();
});
it('should have a div.table-body ', () => {
const flowDe: DebugElement = fixture.debugElement;
const divDe = flowDe.query(By.css('div#ov-flow div.table-body'));
expect(divDe).toBeTruthy();
});
});

View File

@ -0,0 +1,151 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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, OnDestroy, OnInit } from '@angular/core';
import { SortDir, TableBaseImpl, TableResponse } from '../../../fw/widget/table.base';
import { WebSocketService } from '../../../fw/remote/websocket.service';
import { LogService } from '../../../log.service';
import { LoadingService } from '../../../fw/layer/loading.service';
import { FnService } from '../../../fw/util/fn.service';
import { ActivatedRoute } from '@angular/router';
import { LionService } from '../../../fw/util/lion.service';
/**
* Model of the response from WebSocket
*/
interface FlowTableResponse extends TableResponse {
flows: Flow[];
}
/**
* Model of the flows returned from the WebSocket
*/
interface Flow {
state: string;
packets: string;
duration: string;
priority: string;
tableName: string;
selector: string;
treatment: string;
appName: string;
}
/**
* ONOS GUI -- Flow View Component
*/
@Component({
selector: 'onos-flow',
templateUrl: './flow.component.html',
styleUrls: ['./flow.component.css', './flow.theme.css', '../../../fw/widget/table.css', '../../../fw/widget/table.theme.css']
})
export class FlowComponent extends TableBaseImpl implements OnInit, OnDestroy {
lionFn; // Function
id: string;
brief: boolean;
selRowAppId: string;
deviceTip: string;
detailTip: string;
briefTip: string;
portTip: string;
groupTip: string;
meterTip: string;
pipeconfTip: string;
constructor(protected fs: FnService,
protected ls: LoadingService,
protected log: LogService,
protected as: ActivatedRoute,
protected wss: WebSocketService,
protected lion: LionService,
) {
super(fs, ls, log, wss, 'flow');
this.as.queryParams.subscribe(params => {
this.id = params['devId'];
});
this.brief = true;
this.payloadParams = {
devId: this.id
};
this.responseCallback = this.flowResponseCb;
this.sortParams = {
firstCol: 'state',
firstDir: SortDir.desc,
secondCol: 'packets',
secondDir: SortDir.asc,
};
// We want doLion() to be called only after the Lion
// service is populated (from the WebSocket)
// If lion is not ready we make do with a dummy function
// As soon a lion gets loaded this function will be replaced with
// the real thing
if (this.lion.ubercache.length === 0) {
this.lionFn = this.dummyLion;
this.lion.loadCbs.set('flows', () => this.doLion());
} else {
this.doLion();
}
this.parentSelCb = this.rowSelection;
}
ngOnInit() {
this.init();
this.log.debug('FlowComponent initialized');
}
ngOnDestroy() {
this.lion.loadCbs.delete('flows');
this.destroy();
this.log.debug('FlowComponent destroyed');
}
flowResponseCb(data: FlowTableResponse) {
this.log.debug('Flow response received for ', data.flows.length, 'flow');
}
briefToggle() {
this.brief = !this.brief;
}
/**
* Read the LION bundle for App and set up the lionFn
*/
doLion() {
this.lionFn = this.lion.bundle('core.view.Flow');
this.deviceTip = this.lionFn('tt_ctl_show_device');
this.detailTip = this.lionFn('tt_ctl_switcth_detailed');
this.briefTip = this.lionFn('tt_ctl_switcth_brief');
this.portTip = this.lionFn('tt_ctl_show_port');
this.groupTip = this.lionFn('tt_ctl_show_group');
this.meterTip = this.lionFn('tt_ctl_show_meter');
this.pipeconfTip = this.lionFn('tt_ctl_show_pipeconf');
}
rowSelection(event: any, selRow: any) {
this.selRowAppId = selRow.appId;
}
}

View File

@ -0,0 +1,80 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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.
*/
/*
ONOS GUI -- Flow View (theme) -- CSS file
*/
/* a "logical" row is made up of 3 "physical" rows -- color as such */
#ov-flow tr:nth-child(6n + 1),
#ov-flow tr:nth-child(6n + 2),
#ov-flow tr:nth-child(6n + 3) {
background-color: #fbfbfb;
}
#ov-flow tr:nth-child(6n + 4),
#ov-flow tr:nth-child(6n + 5),
#ov-flow tr:nth-child(6n) {
background-color: #f4f4f4;
}
/* highlighted color */
#ov-flow tr:nth-child(6n + 1).data-change,
#ov-flow tr:nth-child(6n + 2).data-change,
#ov-flow tr:nth-child(6n + 3).data-change,
#ov-flow tr:nth-child(6n + 4).data-change,
#ov-flow tr:nth-child(6n + 5).data-change,
#ov-flow tr:nth-child(6n).data-change {
background-color: #FDFFDC;
}
#ov-flow td.selector,
#ov-flow td.treatment {
opacity: 0.65;
}
/* ========== DARK Theme ========== */
.dark #ov-flow tr:nth-child(6n + 1),
.dark #ov-flow tr:nth-child(6n + 2),
.dark #ov-flow tr:nth-child(6n + 3) {
background-color: #333333;
}
.dark #ov-flow tr:nth-child(6n + 4),
.dark #ov-flow tr:nth-child(6n + 5),
.dark #ov-flow tr:nth-child(6n) {
background-color: #3a3a3a;
}
.dark #ov-flow tr:nth-child(6n + 1).data-change,
.dark #ov-flow tr:nth-child(6n + 2).data-change,
.dark #ov-flow tr:nth-child(6n + 3).data-change,
.dark #ov-flow tr:nth-child(6n + 4).data-change,
.dark #ov-flow tr:nth-child(6n + 5).data-change,
.dark #ov-flow tr:nth-child(6n).data-change {
background-color: #423708;
}
.light #flow-details-panel .bottom th {
background-color: #e5e5e6;
}
.light #flow-details-panel .bottom tr:nth-child(odd) {
background-color: #fbfbfb;
}
.light #flow-details-panel .bottom tr:nth-child(even) {
background-color: #f4f4f4;
}

View File

@ -0,0 +1,66 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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.
*/
#flow-details-panel.floatpanel {
z-index: 0;
padding-top: 10px;
font-size: 10pt;
top: 185px;
}
#flow-details-panel .container {
padding: 8px 12px;
}
#flow-details-panel .close-btn {
position: absolute;
right: 5px;
top: 5px;
cursor: pointer;
}
#flow-details-panel .flow-icon {
display: inline-block;
padding: 0 6px 0 0;
vertical-align: middle;
}
#flow-details-panel h2 {
display: inline-block;
margin: 8px 0;
font-weight: bold;
font-size: 16pt;
}
#flow-details-panel hr {
clear: both;
width: 100%;
margin: 2px auto;
}
#flow-details-panel td.label {
font-weight: bold;
text-align: right;
padding-right: 6px;
}
#flow-details-panel .scroll {
border-spacing: 0;
height: 400px;
width: 520px;
overflow: auto;
display: block;
}

View File

@ -0,0 +1,116 @@
<!--
~ Copyright 2018-present Open Networking Foundation
~
~ 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.
-->
<div id="flow-details-panel" class="floatpanel" [@flowDetailsState]="flowId!=='' && !closed">
<div class="container">
<div class="top">
<div class="close-btn">
<onos-icon class="close-btn" classes="active-close" iconId="close" iconSize="20" (click)="close()"></onos-icon>
</div>
<div class="flow-icon">
<onos-icon classes="details-icon" iconId="flowTable" [iconSize]="42"></onos-icon>
</div>
<h2>{{ flowId }}</h2>
<div class="scroll">
<div class="top-content">
<table>
<tbody>
<tr>
<td class="label">{{ lionFn('flowId') }} :</td>
<td class="value">{{ flowId }}</td>
</tr>
<tr>
<td class="label">{{ lionFn('state') }} :</td>
<td class="value">{{ detailsData.state }}</td>
</tr>
<tr>
<td class="label">{{ lionFn('bytes') }} :</td>
<td class="value">{{ detailsData.bytes }}</td>
</tr>
<tr>
<td class="label">{{ lionFn('packets') }} :</td>
<td class="value">{{ detailsData.packets }}</td>
</tr>
<tr>
<td class="label">{{ lionFn('duration') }} :</td>
<td class="value">{{ detailsData.duration }}</td>
</tr>
<tr>
<td class="label">{{ lionFn('priority') }} :</td>
<td class="value">{{ detailsData.priority }}</td>
</tr>
<tr>
<td class="label">{{ lionFn('tableName') }} :</td>
<td class="value">{{ detailsData.tableName }}</td>
</tr>
<tr>
<td class="label">{{ lionFn('appName') }} :</td>
<td class="value">{{ detailsData.appName }}</td>
</tr>
<tr>
<td class="label">{{ lionFn('appId') }} :</td>
<td class="value">{{ detailsData.appId }}</td>
</tr>
<tr>
<td class="label">{{ lionFn('groupId') }} :</td>
<td class="value">{{ detailsData.groupId }}</td>
</tr>
<tr>
<td class="label">{{ lionFn('idleTimeout') }} :</td>
<td class="value">{{ detailsData.idleTimeout }}</td>
</tr>
<tr>
<td class="label">{{ lionFn('hardTimeout') }} :</td>
<td class="value">{{ detailsData.hardTimeout }}</td>
</tr>
<tr>
<td class="label">{{ lionFn('permanent') }} :</td>
<td class="value">{{ detailsData.permanent }}</td>
</tr>
</tbody>
</table>
</div>
<hr>
<h3>{{ lionFn('selector') }}</h3>
<div class="top-content">
<table>
<tbody>
<tr>
<td class="label">ETH_TYPE :</td>
<td class="value">{{ detailsData.selector }}</td>
</tr>
</tbody>
</table>
</div>
<hr>
<h3>{{ lionFn('treatment') }}</h3>
<div class="top-content">
<table>
<tbody>
<tr>
<td class="label">[imm]OUTPUT :</td>
<td class="value">{{ immed(detailsData.treatment) }}</td>
</tr>
<tr>
<td class="label">Clear deferred :</td>
<td class="value">{{ clearDef(detailsData.treatment) }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,140 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FlowDetailsComponent } from './flowdetails.component';
import { ActivatedRoute, Params } from '@angular/router';
import { of } from 'rxjs';
import { FnService } from '../../../../fw/util/fn.service';
import { LogService } from '../../../../log.service';
import { IconService } from '../../../../fw/svg/icon.service';
import { WebSocketService } from '../../../../fw/remote/websocket.service';
import { IconComponent } from '../../../../fw/svg/icon/icon.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';
class MockActivatedRoute extends ActivatedRoute {
constructor(params: Params) {
super();
this.queryParams = of(params);
}
}
class MockIconService {
classes = 'active-close';
loadIconDef() { }
}
class MockWebSocketService {
createWebSocket() { }
isConnected() { return false; }
unbindHandlers() { }
bindHandlers() { }
}
describe('FlowDetailsComponent', () => {
let fs: FnService;
let ar: MockActivatedRoute;
let windowMock: Window;
let logServiceSpy: jasmine.SpyObj<LogService>;
let component: FlowDetailsComponent;
let fixture: ComponentFixture<FlowDetailsComponent>;
beforeEach(async(() => {
const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
ar = new MockActivatedRoute({ 'debug': 'panel' });
windowMock = <any>{
location: <any>{
hostname: 'foo',
host: 'foo',
port: '80',
protocol: 'http',
search: { debug: 'true' },
href: 'ws://foo:123/onos/ui/websock/path',
absUrl: 'ws://foo:123/onos/ui/websock/path'
}
};
fs = new FnService(ar, logSpy, windowMock);
TestBed.configureTestingModule({
imports: [BrowserAnimationsModule],
declarations: [FlowDetailsComponent, IconComponent],
providers: [
{ provide: FnService, useValue: fs },
{ provide: IconService, useClass: MockIconService },
{ provide: LogService, useValue: logSpy },
{ provide: WebSocketService, useClass: MockWebSocketService },
{ provide: 'Window', useValue: windowMock },
]
})
.compileComponents();
logServiceSpy = TestBed.get(LogService);
}));
beforeEach(() => {
fixture = TestBed.createComponent(FlowDetailsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should have an div.close-btn div.top inside a div.container', () => {
const flowDe: DebugElement = fixture.debugElement;
const divDe = flowDe.query(By.css('div.container div.top div.close-btn'));
expect(divDe).toBeTruthy();
});
it('should have a div.flow-icon inside a div.top inside a div.container', () => {
const flowDe: DebugElement = fixture.debugElement;
const divDe = flowDe.query(By.css('div.container div.top div.flow-icon'));
const div: HTMLElement = divDe.nativeElement;
expect(div.textContent).toEqual('');
});
it('should have a div.top-content inside a div.top inside a div.container', () => {
const flowDe: DebugElement = fixture.debugElement;
const divDe = flowDe.query(By.css('div.container div.top div.top-content'));
expect(divDe).toBeTruthy();
});
it('should have a div.scroll inside a div.container', () => {
const flowDe: DebugElement = fixture.debugElement;
const divDe = flowDe.query(By.css('div.container div.scroll'));
expect(divDe).toBeTruthy();
});
it('should have a h2 inside a div.top inside a div.container', () => {
const flowDe: DebugElement = fixture.debugElement;
const divDe = flowDe.query(By.css('div.container div.top h2'));
expect(divDe).toBeTruthy();
});
it('should have a h3 inside a div.scroll inside a div.top inside a div.container', () => {
const flowDe: DebugElement = fixture.debugElement;
const divDe = flowDe.query(By.css('div.container div.top div.scroll h3'));
expect(divDe).toBeTruthy();
});
it('should have a hr inside a div.scroll inside a div.top inside a div.container', () => {
const flowDe: DebugElement = fixture.debugElement;
const divDe = flowDe.query(By.css('div.container div.top div.scroll hr'));
expect(divDe).toBeTruthy();
});
});

View File

@ -0,0 +1,158 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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, OnDestroy, OnChanges, Input, Output, EventEmitter } from '@angular/core';
import { DetailsPanelBaseImpl } from '../../../../fw/widget/detailspanel.base';
import { FnService } from '../../../../fw/util/fn.service';
import { LoadingService } from '../../../../fw/layer/loading.service';
import { LogService } from '../../../../log.service';
import { WebSocketService } from '../../../../fw/remote/websocket.service';
import { LionService } from '../../../../fw/util/lion.service';
import { trigger, state, style, transition, animate } from '@angular/animations';
/**
* The details view when a flow is clicked from the flows view
*
* This is expected to be passed an 'id' and it makes a call
* to the WebSocket with a flowDetailsRequest, and gets back a
* flowDetailsResponse.
*
* The animated fly-in is controlled by the animation below
* The flowDetailsState is attached to flow-details-panel
* and is false (flies out) when id='' and true (flies in) when
* id has a value
*/
@Component({
selector: 'onos-flowdetails',
templateUrl: './flowdetails.component.html',
styleUrls: [
'./flowdetails.component.css',
'../../../../fw/widget/panel.css', '../../../../fw/widget/panel-theme.css'
],
animations: [
trigger('flowDetailsState', [
state('true', style({
transform: 'translateX(-100%)',
opacity: '100'
})),
state('false', style({
transform: 'translateX(0%)',
opacity: '0'
})),
transition('0 => 1', animate('100ms ease-in')),
transition('1 => 0', animate('100ms ease-out'))
])
]
})
export class FlowDetailsComponent extends DetailsPanelBaseImpl implements OnInit, OnDestroy, OnChanges {
@Input() flowId: string;
@Input() appId: string;
@Output() closeEvent = new EventEmitter<string>();
lionFn; // Function
constructor(
protected fs: FnService,
protected ls: LoadingService,
protected log: LogService,
protected wss: WebSocketService,
protected lion: LionService,
) {
super(fs, ls, log, wss, 'flow');
if (this.lion.ubercache.length === 0) {
this.lionFn = this.dummyLion;
this.lion.loadCbs.set('flowdetails', () => this.doLion());
} else {
this.doLion();
}
}
/**
* There is a possibility that a previous selection
* is already registered for call - if so wait 100ms
* for it to deregister - this is because in the list of
* flows we might have selected one higher up the list and
* it is now being processed here before an older selection
* farther down the list has been removed
*/
ngOnInit() {
this.init();
this.log.debug('Flow Details Component initialized:', this.flowId);
}
/**
* Stop listening to flowDetailsResponse on WebSocket
*/
ngOnDestroy() {
this.lion.loadCbs.delete('flowdetails');
this.destroy();
this.log.debug('Flow Details Component destroyed');
}
/**
* Details Panel Data Request on row selection changes
* Should be called whenever flow id changes
* If flowId or appId is empty, no request is made
*/
ngOnChanges() {
if (this.flowId === '' || this.appId === '') {
return;
} else {
const query = {
'flowId': this.flowId,
'appId': this.appId
};
this.requestDetailsPanelData(query);
}
}
/**
* Read the LION bundle for Flow and set up the lionFn
*/
doLion() {
this.lionFn = this.lion.bundle('core.view.Flow');
}
/**
* Return immediate value of flow treatment on flow details request
*/
immed(treatmentData: any) {
if (treatmentData === undefined) {
return '';
} else {
return treatmentData.immed;
}
}
/**
* Return clear deferred value of flow treatment on flow details request
*/
clearDef(treatmentData: any) {
if (treatmentData === undefined) {
return '';
} else {
return treatmentData.clearDef;
}
}
close() {
this.flowId = null;
this.appId = null;
this.closed = true;
this.closeEvent.emit(this.flowId);
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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 { Routes, RouterModule } from '@angular/router';
import { GroupComponent } from './group/group.component';
const routes: Routes = [
{
path: '',
component: GroupComponent
}
];
/**
* ONOS GUI -- Groups Tabular View Feature Routing Module - allows it to be lazy loaded
*
* See https://angular.io/guide/lazy-loading-ngmodules
*/
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class GroupRoutingModule { }

View File

@ -0,0 +1,38 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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 { CommonModule } from '@angular/common';
import { GroupRoutingModule } from './group-routing.module';
import { GroupComponent } from './group/group.component';
import { SvgModule } from '../../fw/svg/svg.module';
import { WidgetModule } from '../../fw/widget/widget.module';
import { FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
@NgModule({
imports: [
CommonModule,
GroupRoutingModule,
SvgModule,
WidgetModule,
FormsModule,
RouterModule
],
declarations: [GroupComponent],
exports: [GroupComponent]
})
export class GroupModule { }

View File

@ -0,0 +1,101 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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.
*/
/*
ONOS GUI -- Group View (layout) -- CSS file
*/
#ov-group h2 {
display: inline-block;
}
#ov-group div.ctrl-btns {
}
#ov-group td {
text-align: center;
}
#ov-group td.right {
text-align: right;
}
#ov-group td.buckets {
text-align: left;
padding-left: 36px;
}
#ov-group .tabular-header {
text-align: left;
}
#ov-group div.summary-list .table-header td {
font-weight: bold;
font-variant: small-caps;
text-transform: uppercase;
font-size: 10pt;
padding-top: 8px;
padding-bottom: 8px;
letter-spacing: 0.02em;
cursor: pointer;
background-color: #e5e5e6;
color: #3c3a3a;
}
/* More in generic panel.css */
#group-details-panel.floatpanel {
z-index: 0;
}
#group-details-panel .container {
padding: 8px 12px;
}
#group-details-panel .close-btn {
position: absolute;
right: 12px;
top: 12px;
cursor: pointer;
}
#group-details-panel .dev-icon {
display: inline-block;
padding: 0 6px 0 0;
vertical-align: middle;
}
#group-details-panel h2 {
display: inline-block;
margin: 8px 0;
font-size: 16pt;
font-weight: lighter;
}
#group-details-panel h3 {
display: inline-block;
margin: 8px 0;
font-size: 11pt;
font-variant: small-caps;
text-transform: uppercase;
}
#group-details-panel .top-content table {
font-size: 10pt;
}
#group-details-panel td.label {
font-weight: bold;
text-align: right;
padding-right: 6px;
}

View File

@ -0,0 +1,124 @@
<!--
~ Copyright 2018-present Open Networking Foundation
~
~ 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.
-->
<div id="ov-group" xmlns="http://www.w3.org/1999/html">
<div class="tabular-header">
<h2>
Groups for Device {{id}} ({{tableData.length}} total)
</h2>
<div class="ctrl-btns">
<div class="refresh" (click)="toggleRefresh()">
<!-- See icon.theme.css for the defintions of the classes active and refresh-->
<onos-icon classes="{{ autoRefresh?'active refresh':'refresh' }}" iconId="refresh" iconSize="42" toolTip="{{ autoRefreshTip }}"></onos-icon>
</div>
<div class="separator"></div>
<span *ngIf="brief" (click)="briefToggle()">
<div>
<onos-icon classes="{{ id ? 'active-rect' :undefined}}" iconId="plus" iconSize="42" toolTip="{{detailTip}}"></onos-icon>
</div>
</span>
<span *ngIf="!brief" (click)="briefToggle()">
<div>
<onos-icon classes="{{ id ? 'active-rect' :undefined}}" iconId="minus" iconSize="42" toolTip="{{briefTip}}"></onos-icon>
</div>
</span>
<div class="separator"></div>
<div routerLink="/device" [queryParams]="{ devId: id }" routerLinkActive="active">
<onos-icon classes="{{ id ? 'active-rect':undefined }}" iconId="deviceTable" iconSize="42" toolTip="{{deviceTip}}"></onos-icon>
</div>
<div routerLink="/flow" [queryParams]="{ devId: id }" routerLinkActive="active">
<onos-icon classes="{{ id ? 'active-rect' :undefined}}" iconId="flowTable" iconSize="42" toolTip="{{ flowTip }}"></onos-icon>
</div>
<div routerLink="/port" [queryParams]="{ devId: id }" routerLinkActive="active">
<onos-icon classes="{{ id ? 'active-rect' :undefined}}" iconId="portTable" iconSize="42" toolTip="{{ portTip }}"></onos-icon>
</div>
<div>
<onos-icon classes="{{ id ? 'current-view' :undefined}}" iconId="groupTable" iconSize="42"></onos-icon>
</div>
<div routerLink="/meter" [queryParams]="{ devId: id }" routerLinkActive="active">
<onos-icon classes="{{ id ? 'active-rect' :undefined}}" iconId="meterTable" iconSize="42" toolTip="{{ meterTip }}"></onos-icon>
</div>
<div routerLink="/pipeconf" [queryParams]="{ devId: id }" routerLinkActive="active">
<onos-icon classes="{{ id ? 'active-rect' :undefined}}" iconId="pipeconfTable" iconSize="42" toolTip="{{ pipeconfTip }}"></onos-icon>
</div>
</div>
<div class="search">
<input id="searchinput" [(ngModel)]="tableDataFilter.queryStr" type="search" #search placeholder="Search" />
<select [(ngModel)]="tableDataFilter.queryBy">
<option value="" disabled>Search By</option>
<option value="$">All Fields</option>
<option value="id">Group Id</option>
<option value="app_id">App Id</option>
<option value="state">State</option>
<option value="type">Type</option>
</select>
</div>
</div>
<div class="summary-list" onosTableResize>
<div class="table-header">
<table>
<tr>
<td colId="id" (click)="onSort('id')">Group Id
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('id')"></onos-icon>
</td>
<td colId="app_id" (click)="onSort('app_id')">App Id
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('app_id')"></onos-icon>
</td>
<td colId="state" (click)="onSort('state')">State
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('state')"></onos-icon>
</td>
<td colId="type" (click)="onSort('type')">Type
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('type')"></onos-icon>
</td>
<td colId="packets" (click)="onSort('packets')">Packets
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('packets')"></onos-icon>
</td>
<td colId="bytes" (click)="onSort('bytes')">Bytes
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('bytes')"></onos-icon>
</td>
</tr>
</table>
</div>
<div class="table-body">
<table>
<tr class="no-data" *ngIf="tableData.length === 0">
<td colspan="6">{{ annots.noRowsMsg }}</td>
</tr>
<ng-template ngFor let-group [ngForOf]="tableData | filter : tableDataFilter">
<tr [ngClass]="{'data-change': isChanged(group.id)}">
<td>{{group.id}}</td>
<td>{{group.app_id}}</td>
<td>{{group.state}}</td>
<td>{{group.type}}</td>
<td>{{group.packets}}</td>
<td>{{group.bytes}}</td>
</tr>
<tr (click)="selectCallback($event, group)" [hidden]="brief" [ngClass]="{'data-change': isChanged(group.id)}">
<td class="buckets" colspan="6" [innerHTML]="group.buckets"></td>
</tr>
</ng-template>
</table>
</div>
</div>
</div>

View File

@ -0,0 +1,175 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { GroupComponent } from './group.component';
import { LogService } from '../../../log.service';
import { ConsoleLoggerService } from '../../../consolelogger.service';
import { IconComponent } from '../../../fw/svg/icon/icon.component';
import { DialogService } from '../../../fw/layer/dialog.service';
import { FnService } from '../../../fw/util/fn.service';
import { IconService } from '../../../fw/svg/icon.service';
import { KeyService } from '../../../fw/util/key.service';
import { LoadingService } from '../../../fw/layer/loading.service';
import { ThemeService } from '../../../fw/util/theme.service';
import { UrlFnService } from '../../../fw/remote/urlfn.service';
import { WebSocketService } from '../../../fw/remote/websocket.service';
import { ActivatedRoute, Params } from '@angular/router';
import { of } from 'rxjs';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FormsModule } from '@angular/forms';
import { TableFilterPipe } from '../../../fw/widget/tablefilter.pipe';
import { RouterTestingModule } from '@angular/router/testing';
import { DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';
class MockActivatedRoute extends ActivatedRoute {
constructor(params: Params) {
super();
this.queryParams = of(params);
}
}
class MockDialogService { }
class MockFnService { }
class MockIconService {
loadIconDef() { }
}
class MockKeyService { }
class MockLoadingService {
startAnim() { }
stop() { }
waiting() { }
}
class MockThemeService { }
class MockUrlFnService { }
class MockWebSocketService {
createWebSocket() { }
isConnected() { return false; }
unbindHandlers() { }
bindHandlers() { }
}
/**
* ONOS GUI -- Group View Module - Unit Tests
*/
describe('GroupComponent', () => {
let component: GroupComponent;
let fixture: ComponentFixture<GroupComponent>;
let log: LogService;
let fs: FnService;
let ar: MockActivatedRoute;
let windowMock: Window;
const bundleObj = {
'core.view.Group': {
test: 'test1',
tt_help: 'Help!'
}
};
const mockLion = (key) => {
return bundleObj[key] || '%' + key + '%';
};
beforeEach(async(() => {
log = new ConsoleLoggerService();
ar = new MockActivatedRoute({ 'debug': 'txrx' });
windowMock = <any>{
location: <any>{
hostname: 'foo',
host: 'foo',
port: '80',
protocol: 'http',
search: { debug: 'true' },
href: 'ws://foo:123/onos/ui/websock/path',
absUrl: 'ws://foo:123/onos/ui/websock/path'
}
};
fs = new FnService(ar, log, windowMock);
TestBed.configureTestingModule({
imports: [BrowserAnimationsModule, FormsModule, RouterTestingModule],
declarations: [GroupComponent, IconComponent, TableFilterPipe],
providers: [
{ provide: DialogService, useClass: MockDialogService },
{ provide: FnService, useValue: fs },
{ provide: IconService, useClass: MockIconService },
{ provide: KeyService, useClass: MockKeyService },
{ provide: LoadingService, useClass: MockLoadingService },
{ provide: LogService, useValue: log },
{ provide: ThemeService, useClass: MockThemeService },
{ provide: UrlFnService, useClass: MockUrlFnService },
{ provide: WebSocketService, useClass: MockWebSocketService },
{ provide: 'Window', useValue: windowMock },
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(GroupComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should have a div.tabular-header inside a div#ov-group', () => {
const groupDe: DebugElement = fixture.debugElement;
const divDe = groupDe.query(By.css('div#ov-group div.tabular-header'));
expect(divDe).toBeTruthy();
});
it('should have a h2 inside the div.tabular-header', () => {
const groupDe: DebugElement = fixture.debugElement;
const divDe = groupDe.query(By.css('div#ov-group div.tabular-header h2'));
const div: HTMLElement = divDe.nativeElement;
expect(div.textContent).toEqual(' Groups for Device (0 total) ');
});
it('should have a refresh button inside the div.tabular-header', () => {
const groupDe: DebugElement = fixture.debugElement;
const divDe = groupDe.query(By.css('div#ov-group div.tabular-header div.ctrl-btns div.refresh'));
expect(divDe).toBeTruthy();
});
it('should have a div.summary-list inside a div#ov-group', () => {
const groupDe: DebugElement = fixture.debugElement;
const divDe = groupDe.query(By.css('div#ov-group div.summary-list'));
expect(divDe).toBeTruthy();
});
it('should have a div.table-header inside a div.summary-list inside a div#ov-group', () => {
const groupDe: DebugElement = fixture.debugElement;
const divDe = groupDe.query(By.css('div#ov-group div.summary-list div.table-header'));
expect(divDe).toBeTruthy();
});
it('should have a div.table-body inside a div.summary-list inside a div#ov-group', () => {
const groupDe: DebugElement = fixture.debugElement;
const divDe = groupDe.query(By.css('div#ov-group div.summary-list div.table-body'));
expect(divDe).toBeTruthy();
});
});

View File

@ -0,0 +1,109 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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, OnDestroy } from '@angular/core';
import { LogService } from '../../../log.service';
import { FnService } from '../../../fw/util/fn.service';
import { LoadingService } from '../../../fw/layer/loading.service';
import { WebSocketService } from '../../../fw/remote/websocket.service';
import { ActivatedRoute } from '@angular/router';
import { TableResponse, TableBaseImpl, SortDir } from '../../../fw/widget/table.base';
/**
* Model of the response from WebSocket
*/
interface GroupTableResponse extends TableResponse {
groups: Group[];
}
/**
* Model of the flows returned from the WebSocket
*/
interface Group {
id: string;
app_id: string;
state: string;
type: string;
packets: string;
bytes: string;
}
/**
* ONOS GUI -- Group View Component
*/
@Component({
selector: 'onos-group',
templateUrl: './group.component.html',
styleUrls: ['./group.component.css', './group.theme.css', '../../../fw/widget/table.css', '../../../fw/widget/table.theme.css']
})
export class GroupComponent extends TableBaseImpl implements OnInit, OnDestroy {
id: string;
brief: boolean;
// TODO: Update for LION
deviceTip = 'Show device table';
detailTip = 'Switch to detailed view';
briefTip = 'Switch to brief view';
flowTip = 'Show flow view for selected device';
portTip = 'Show port view for selected device';
meterTip = 'Show meter view for selected device';
pipeconfTip = 'Show pipeconf view for selected device';
constructor(
protected log: LogService,
protected fs: FnService,
protected ls: LoadingService,
protected wss: WebSocketService,
protected ar: ActivatedRoute,
) {
super(fs, ls, log, wss, 'group');
this.ar.queryParams.subscribe(params => {
this.id = params['devId'];
});
this.brief = true;
this.payloadParams = {
devId: this.id
};
this.responseCallback = this.groupResponseCb;
this.sortParams = {
firstCol: 'id',
firstDir: SortDir.desc,
secondCol: 'app_id',
secondDir: SortDir.asc,
};
}
ngOnInit() {
this.init();
this.log.info('GroupComponent initialized');
}
ngOnDestroy() {
this.destroy();
this.log.info('GroupComponent destroyed');
}
groupResponseCb(data: GroupTableResponse) {
this.log.debug('Group response received for ', data.groups.length, 'group');
}
briefToggle() {
this.brief = !this.brief;
}
}

View File

@ -0,0 +1,71 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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.
*/
/*
ONOS GUI -- Group View (theme) -- CSS file
*/
/* a "logical" row is made up of 2 "physical" rows -- color as such */
#ov-group tr:nth-child(4n + 1),
#ov-group tr:nth-child(4n + 2) {
background-color: #fbfbfb;
}
#ov-group tr:nth-child(4n + 3),
#ov-group tr:nth-child(4n) {
background-color: #f4f4f4;
}
/* highlighted color */
#ov-group tr:nth-child(4n + 1).data-change,
#ov-group tr:nth-child(4n + 2).data-change,
#ov-group tr:nth-child(4n + 3).data-change,
#ov-group tr:nth-child(4n).data-change {
background-color: #FDFFDC;
}
#ov-group td.selector,
#ov-group td.treatment {
opacity: 0.65;
}
/* ========== DARK Theme ========== */
.dark #ov-group tr:nth-child(4n + 1),
.dark #ov-group tr:nth-child(4n + 2) {
background-color: #333333;
}
.dark #ov-group tr:nth-child(4n + 3),
.dark #ov-group tr:nth-child(4n) {
background-color: #3a3a3a;
}
.dark #ov-group tr:nth-child(4n + 1).data-change,
.dark #ov-group tr:nth-child(4n + 2).data-change,
.dark #ov-group tr:nth-child(4n + 3).data-change,
.dark #ov-group tr:nth-child(4n).data-change {
background-color: #423708;
}
.light #group-details-panel .bottom th {
background-color: #e5e5e6;
}
.light #group-details-panel .bottom tr:nth-child(odd) {
background-color: #fbfbfb;
}
.light #group-details-panel .bottom tr:nth-child(even) {
background-color: #f4f4f4;
}

View File

@ -0,0 +1,31 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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 { Routes, RouterModule } from '@angular/router';
import { HostComponent } from './host/host.component';
const hostRoutes: Routes = [
{
path: '',
component: HostComponent
}
];
@NgModule({
imports: [RouterModule.forChild(hostRoutes)],
exports: [RouterModule]
})
export class HostRoutingModule { }

View File

@ -0,0 +1,34 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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 { CommonModule } from '@angular/common';
import { HostRoutingModule } from './host-routing.module';
import { HostComponent } from './host/host.component';
import { SvgModule } from '../../fw/svg/svg.module';
import { WidgetModule } from '../../fw/widget/widget.module';
import { HostDetailsComponent } from './hostdetails/hostdetails.component';
@NgModule({
imports: [
CommonModule,
HostRoutingModule,
WidgetModule,
SvgModule
],
declarations: [HostComponent, HostDetailsComponent]
})
export class HostModule { }

View File

@ -0,0 +1,43 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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.
*/
/*
ONOS GUI -- Hosts Panel (layout) -- CSS file
*/
#ov-host .tabular-header {
text-align: left;
}
#ov-host div.summary-list .table-header td {
font-weight: bold;
font-variant: small-caps;
text-transform: uppercase;
font-size: 10pt;
padding-top: 8px;
padding-bottom: 8px;
letter-spacing: 0.02em;
cursor: pointer;
background-color: #e5e5e6;
}
#ov-host h2 {
display: inline-block;
}
#ov-host th, td {
text-align: left;
padding: 8px;
}

View File

@ -0,0 +1,76 @@
<!--
~ Copyright 2018-present Open Networking Foundation
~
~ 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.
-->
<div id="ov-host">
<div class="tabular-header">
<h2>Hosts ({{tableData.length}} total)</h2>
<div class="ctrl-btns">
<div class="refresh" (click)="toggleRefresh()">
<!-- See icon.theme.css for the defintions of the classes active and refresh-->
<onos-icon classes="{{ autoRefresh?'active refresh':'refresh' }}" iconId="refresh" iconSize="42" toolTip="{{ autoRefreshTip }}"></onos-icon>
</div>
</div>
</div>
<div class="summary-list" onosTableResize>
<div class="table-header">
<table>
<tr>
<td colId="type" class="table-icon"></td>
<td colId="name" (click)="onSort('name')">Friendly Name
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('name')"></onos-icon>
</td>
<td colId="id" (click)="onSort('id')">Host ID
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('id')"></onos-icon>
</td>
<td colId="mac" (click)="onSort('mac')">MAC Address
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('mac')"></onos-icon>
</td>
<td colId="vlan" (click)="onSort('vlan')">VLAN ID
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('vlan')"></onos-icon>
</td>
<td colId="configured" (click)="onSort('configured')">Configured
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('configured')"></onos-icon>
</td>
<td colId="ips" (click)="onSort('ips')">IP Addresses
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('ips')"></onos-icon>
</td>
<td colId="location" (click)="onSort('location')">Location
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('location')"></onos-icon>
</td>
</tr>
</table>
</div>
<div class="table-body">
<table>
<tr *ngIf="tableData.length === 0" class="no-data">
<td colspan="8">{{ annots.noRowsMsg }}</td>
</tr>
<tr *ngFor="let host of tableData" (click)="selectCallback($event, host)" [ngClass]="{selected: host.id === selId, 'data-change': isChanged(host.id)}">
<td class="table-icon">
<onos-icon classes="{{host._iconid_type? 'active-type':undefined}}" iconId="{{host._iconid_type}}"></onos-icon>
</td>
<td>{{host.name}}</td>
<td>{{host.id}}</td>
<td>{{host.mac}}</td>
<td>{{host.vlan}}</td>
<td>{{host.configured}}</td>
<td>{{host.ips}}</td>
<td>{{host.location}}</td>
</tr>
</table>
</div>
</div>
<onos-hostdetails class="floatpanels" id="{{ selId }}" (closeEvent)="deselectRow($event)"></onos-hostdetails>
</div>

View File

@ -0,0 +1,176 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ActivatedRoute, Params } from '@angular/router';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FormsModule } from '@angular/forms';
import { DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';
import { LogService } from '../../../log.service';
import { HostComponent } from './host.component';
import { HostDetailsComponent } from '../hostdetails/hostdetails.component';
import { DialogService } from '../../../fw/layer/dialog.service';
import { FnService } from '../../../fw/util/fn.service';
import { IconComponent } from '../../../fw/svg/icon/icon.component';
import { IconService } from '../../../fw/svg/icon.service';
import { KeyService } from '../../../fw/util/key.service';
import { LoadingService } from '../../../fw/layer/loading.service';
import { ThemeService } from '../../../fw/util/theme.service';
import { TableFilterPipe } from '../../../fw/widget/tablefilter.pipe';
import { UrlFnService } from '../../../fw/remote/urlfn.service';
import { WebSocketService } from '../../../fw/remote/websocket.service';
import { of } from 'rxjs';
import { } from 'jasmine';
class MockActivatedRoute extends ActivatedRoute {
constructor(params: Params) {
super();
this.queryParams = of(params);
}
}
class MockDialogService { }
class MockFnService { }
class MockIconService {
loadIconDef() { }
}
class MockKeyService { }
class MockLoadingService {
startAnim() { }
stop() { }
waiting() { }
}
class MockThemeService { }
class MockUrlFnService { }
class MockWebSocketService {
createWebSocket() { }
isConnected() { return false; }
unbindHandlers() { }
bindHandlers() { }
}
describe('HostComponent', () => {
let fs: FnService;
let ar: MockActivatedRoute;
let windowMock: Window;
let logServiceSpy: jasmine.SpyObj<LogService>;
let component: HostComponent;
let fixture: ComponentFixture<HostComponent>;
const bundleObj = {
'core.view.Host': {
test: 'test1'
}
};
const mockLion = (key) => {
return bundleObj[key] || '%' + key + '%';
};
beforeEach(async(() => {
const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
ar = new MockActivatedRoute({ 'debug': 'txrx' });
windowMock = <any>{
location: <any>{
hostname: 'foo',
host: 'foo',
port: '80',
protocol: 'http',
search: { debug: 'true' },
href: 'ws://foo:123/onos/ui/websock/path',
absUrl: 'ws://foo:123/onos/ui/websock/path'
}
};
fs = new FnService(ar, logSpy, windowMock);
TestBed.configureTestingModule({
imports: [BrowserAnimationsModule, FormsModule],
declarations: [HostComponent, HostDetailsComponent, IconComponent, TableFilterPipe],
providers: [
{ provide: DialogService, useClass: MockDialogService },
{ provide: FnService, useValue: fs },
{ provide: IconService, useClass: MockIconService },
{ provide: KeyService, useClass: MockKeyService },
{ provide: LoadingService, useClass: MockLoadingService },
{ provide: LogService, useValue: logSpy },
{ provide: ThemeService, useClass: MockThemeService },
{ provide: UrlFnService, useClass: MockUrlFnService },
{ provide: WebSocketService, useClass: MockWebSocketService },
{ provide: 'Window', useValue: windowMock },
]
})
.compileComponents();
logServiceSpy = TestBed.get(LogService);
}));
beforeEach(() => {
fixture = TestBed.createComponent(HostComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should have a div.tabular-header inside a div#ov-host', () => {
const hostDe: DebugElement = fixture.debugElement;
const divDe = hostDe.query(By.css('div#ov-host div.tabular-header'));
expect(divDe).toBeTruthy();
});
it('should have a h2 inside the div.tabular-header', () => {
const hostDe: DebugElement = fixture.debugElement;
const divDe = hostDe.query(By.css('div#ov-host div.tabular-header h2'));
const div: HTMLElement = divDe.nativeElement;
expect(div.textContent).toEqual('Hosts (0 total)');
});
it('should have a refresh button inside the div.tabular-header', () => {
const hostDe: DebugElement = fixture.debugElement;
const divDe = hostDe.query(By.css('div#ov-host div.tabular-header div.ctrl-btns div.refresh'));
expect(divDe).toBeTruthy();
});
it('should have a div.summary-list inside a div#ov-host', () => {
const hostDe: DebugElement = fixture.debugElement;
const divDe = hostDe.query(By.css('div#ov-host div.summary-list'));
expect(divDe).toBeTruthy();
});
it('should have a div.table-header inside a div.summary-list inside a div#ov-host', () => {
const hostDe: DebugElement = fixture.debugElement;
const divDe = hostDe.query(By.css('div#ov-host div.summary-list div.table-header'));
expect(divDe).toBeTruthy();
});
it('should have a div.table-body inside a div.summary-list inside a div#ov-host', () => {
const hostDe: DebugElement = fixture.debugElement;
const divDe = hostDe.query(By.css('div#ov-host div.summary-list div.table-body'));
expect(divDe).toBeTruthy();
});
});

View File

@ -0,0 +1,79 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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, OnDestroy } from '@angular/core';
import { FnService } from '../../../fw/util/fn.service';
import { LoadingService } from '../../../fw/layer/loading.service';
import { LogService } from '../../../log.service';
import { TableBaseImpl, TableResponse, SortDir } from '../../../fw/widget/table.base';
import { WebSocketService } from '../../../fw/remote/websocket.service';
interface HostTableResponse extends TableResponse {
hosts: Host[];
}
interface Host {
name: boolean;
id: string;
hw: string;
vlanId: string;
configured: string;
address: string;
location: string;
_iconid_type: string;
}
/**
* ONOS GUI -- Host View Component
*/
@Component({
selector: 'onos-host',
templateUrl: './host.component.html',
styleUrls: ['./host.component.css',
'../../../fw/widget/table.css', '../../../fw/widget/table.theme.css']
})
export class HostComponent extends TableBaseImpl implements OnInit, OnDestroy {
constructor(
protected fs: FnService,
protected ls: LoadingService,
protected log: LogService,
protected wss: WebSocketService,
) {
super(fs, ls, log, wss, 'host');
this.responseCallback = this.hostResponseCb;
this.sortParams = {
firstCol: 'name',
firstDir: SortDir.desc,
secondCol: 'id',
secondDir: SortDir.asc,
};
}
ngOnInit() {
this.init();
this.log.debug('HostComponent initialized');
}
ngOnDestroy() {
this.destroy();
this.log.debug('HostComponent destroyed');
}
hostResponseCb(data: HostTableResponse) {
this.log.debug('Host response received for ', data.hosts.length, 'host');
}
}

View File

@ -0,0 +1,86 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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.
*/
/*
ONOS GUI -- Hosts Details Panel (layout) -- CSS file
*/
#host-details-panel.floatpanel {
z-index: 0;
font-size: 10pt;
top: 145px;
height: 80vh;
}
#host-details-panel.floatpanel a {
font-weight: bold;
}
#host-details-panel .host-details {
padding: 0 30px;
}
#host-details-panel .close-btn {
position: absolute;
right: 5px;
top: 5px;
cursor: pointer;
}
#host-details-panel .host-icon {
display: inline-block;
padding: 0 6px 0 0;
vertical-align: middle;
}
#host-details-panel h2 {
display: inline-block;
margin: 8px 0;
font-weight: bold;
font-size: 16pt;
}
#host-details-panel h2 input {
font-size: 0.90em;
width: 106%;
}
#host-details-panel .actionBtns div {
padding: 12px 6px;
}
#host-details-panel hr {
width: 100%;
margin: 2px auto;
}
#host-details-panel td.label {
font-weight: bold;
text-align: right;
padding-right: 6px;
}
#host-details-panel .editable {
border-bottom: 1px dashed #ca504b;
}
#host-details-panel .clickable {
cursor: pointer;
}
#host-details-panel .bottom thead tr {
background-color: #e5e5e6;
}

View File

@ -0,0 +1,70 @@
<!--
~ Copyright 2018-present Open Networking Foundation
~
~ 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.
-->
<div id="host-details-panel" class="floatpanel" [@hostDetailsState]="id!=='' && !closed">
<div class="container">
<div class="top">
<onos-icon class="close-btn" classes="active-close" iconId="close" iconSize="20" (click)="close()"></onos-icon>
</div>
<div class="host-icon">
<onos-icon classes="{{detailsData._iconid_type? 'hostIcon_endstation':undefined}}" iconId="{{detailsData._iconid_type}}"
[iconSize]="40"></onos-icon>
</div>
<h2 class="editable clickable">{{detailsData.name}}</h2>
<div class="top-content">
<div class="top-tables">
<div class="left">
<table>
<tbody>
<tr>
<td class="label" width="110">Host ID :</td>
<td class="value" width="80">{{detailsData.id}}</td>
</tr>
<tr>
<td class="label" width="110">IP Address :</td>
<td class="value" width="80">{{detailsData.ips}}</td>
</tr>
<tr>
<td class="label" width="110">MAC Address :</td>
<td class="value" width="80">{{detailsData.mac}}</td>
</tr>
</tbody>
</table>
</div>
<div class="right">
<table>
<tbody>
<tr>
<td class="label" width="110">VLAN :</td>
<td class="value" width="80">{{detailsData.vlan}}</td>
</tr>
<tr>
<td class="label" width="110">Configured :</td>
<td class="value" width="80">{{detailsData.configured}}</td>
</tr>
<tr>
<td class="label" width="110">Location :</td>
<td class="value" width="80">{{detailsData.location}}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<hr>
<div class="bottom"></div>
</div>
</div>

View File

@ -0,0 +1,167 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ActivatedRoute, Params } from '@angular/router';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';
import { LogService } from '../../../log.service';
import { FnService } from '../../../../app/fw/util/fn.service';
import { IconComponent } from '../../../../app/fw/svg/icon/icon.component';
import { IconService } from '../../../../app/fw/svg/icon.service';
import { UrlFnService } from '../../../fw/remote/urlfn.service';
import { WebSocketService } from '../../../fw/remote/websocket.service';
import { of } from 'rxjs';
import { } from 'jasmine';
import { HostDetailsComponent } from './hostdetails.component';
class MockActivatedRoute extends ActivatedRoute {
constructor(params: Params) {
super();
this.queryParams = of(params);
}
}
class MockFnService { }
class MockIconService {
loadIconDef() { }
}
class MockUrlFnService { }
class MockWebSocketService {
createWebSocket() { }
isConnected() { return false; }
unbindHandlers() { }
bindHandlers() { }
}
/**
* ONOS GUI -- Host Detail Panel View -- Unit Tests
*/
describe('HostdetailsComponent', () => {
let fs: FnService;
let ar: MockActivatedRoute;
let windowMock: Window;
let logServiceSpy: jasmine.SpyObj<LogService>;
let component: HostDetailsComponent;
let fixture: ComponentFixture<HostDetailsComponent>;
const bundleObj = {
'core.view.Hosts': {
}
};
const mockLion = (key) => {
return bundleObj[key] || '%' + key + '%';
};
beforeEach(async(() => {
const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
ar = new MockActivatedRoute({ 'debug': 'panel' });
windowMock = <any>{
location: <any>{
hostname: 'foo',
host: 'foo',
port: '80',
protocol: 'http',
search: { debug: 'true' },
href: 'ws://foo:123/onos/ui/websock/path',
absUrl: 'ws://foo:123/onos/ui/websock/path'
}
};
fs = new FnService(ar, logSpy, windowMock);
TestBed.configureTestingModule({
imports: [BrowserAnimationsModule],
declarations: [HostDetailsComponent, IconComponent],
providers: [
{ provide: FnService, useValue: fs },
{ provide: IconService, useClass: MockIconService },
{ provide: LogService, useValue: logSpy },
{ provide: UrlFnService, useClass: MockUrlFnService },
{ provide: WebSocketService, useClass: MockWebSocketService },
{ provide: 'Window', useValue: windowMock },
]
})
.compileComponents();
logServiceSpy = TestBed.get(LogService);
}));
beforeEach(() => {
fixture = TestBed.createComponent(HostDetailsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should have an onos-icon.close-btn inside a div.top inside a div.container', () => {
const hostDe: DebugElement = fixture.debugElement;
const divDe = hostDe.query(By.css('div.container div.top onos-icon.close-btn'));
expect(divDe).toBeTruthy();
});
it('should have a div.host-icon inside a div.container', () => {
const hostDe: DebugElement = fixture.debugElement;
const divDe = hostDe.query(By.css('div.container div.host-icon'));
expect(divDe).toBeTruthy();
});
it('should have a h2 inside the div.container', () => {
const hostDe: DebugElement = fixture.debugElement;
const divDe = hostDe.query(By.css('div#host-details-panel div.container h2'));
const div: HTMLElement = divDe.nativeElement;
expect(div.textContent).toEqual('');
});
it('should have a div.top-content inside a div.container', () => {
const hostDe: DebugElement = fixture.debugElement;
const divDe = hostDe.query(By.css('div.container div.top-content'));
expect(divDe).toBeTruthy();
});
it('should have a div.top-tables inside a div.top-content inside a div.container', () => {
const hostDe: DebugElement = fixture.debugElement;
const divDe = hostDe.query(By.css('div.container div.top-content div.top-tables'));
expect(divDe).toBeTruthy();
});
it('should have a div.left inside a div.top-tables inside a div.top-content inside a div.container', () => {
const hostDe: DebugElement = fixture.debugElement;
const divDe = hostDe.query(By.css('div.container div.top-content div.top-tables div.left'));
expect(divDe).toBeTruthy();
});
it('should have a div.right inside a div.top-tables inside a div.top-content inside a div.container', () => {
const hostDe: DebugElement = fixture.debugElement;
const divDe = hostDe.query(By.css('div.container div.top-content div.top-tables div.right'));
expect(divDe).toBeTruthy();
});
it('should have a div.bottom inside a div.container', () => {
const hostDe: DebugElement = fixture.debugElement;
const divDe = hostDe.query(By.css('div.container div.bottom'));
expect(divDe).toBeTruthy();
});
});

View File

@ -0,0 +1,98 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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, Input, OnInit, OnDestroy, OnChanges } from '@angular/core';
import { trigger, state, style, animate, transition } from '@angular/animations';
import { FnService } from '../../../fw/util/fn.service';
import { LoadingService } from '../../../fw/layer/loading.service';
import { LogService } from '../../../log.service';
import { WebSocketService } from '../../../fw/remote/websocket.service';
import { DetailsPanelBaseImpl } from '../../../fw/widget/detailspanel.base';
/**
* The details view when a host row is clicked from the Host view
*
* This is expected to be passed an 'id' and it makes a call
* to the WebSocket with an hostDetailsRequest, and gets back an
* hostDetailsResponse.
*
* The animated fly-in is controlled by the animation below
* The hostDetailsState is attached to host-details-panel
* and is false (flies out) when id='' and true (flies in) when
* id has a value
*/
@Component({
selector: 'onos-hostdetails',
templateUrl: './hostdetails.component.html',
styleUrls: ['./hostdetails.component.css',
'../../../fw/widget/panel.css', '../../../fw/widget/panel-theme.css'
],
animations: [
trigger('hostDetailsState', [
state('true', style({
transform: 'translateX(-100%)',
opacity: '100'
})),
state('false', style({
transform: 'translateX(0%)',
opacity: '0'
})),
transition('0 => 1', animate('100ms ease-in')),
transition('1 => 0', animate('100ms ease-out'))
])
]
})
export class HostDetailsComponent extends DetailsPanelBaseImpl implements OnInit, OnDestroy, OnChanges {
@Input() id: string;
constructor(
protected fs: FnService,
protected ls: LoadingService,
protected log: LogService,
protected wss: WebSocketService
) {
super(fs, ls, log, wss, 'host');
}
ngOnInit() {
this.init();
this.log.debug('Hosts Details Component initialized:', this.id);
}
ngOnDestroy() {
this.destroy();
this.log.debug('Hosts Details Component destroyed');
}
/**
* Details Panel Data Request on row selection changes
* Should be called whenever id changes
* If id is empty, no request is made
*/
ngOnChanges() {
if (this.id === '') {
return '';
} else {
const query = {
'id': this.id
};
this.requestDetailsPanelData(query);
}
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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 { Routes, RouterModule } from '@angular/router';
import { LinkComponent } from './link/link.component';
const linkRoutes: Routes = [
{
path: '',
component: LinkComponent
}
];
/**
* ONOS GUI -- Links Tabular View Feature Routing Module - allows it to be lazy loaded
*
* See https://angular.io/guide/lazy-loading-ngmodules
*/
@NgModule({
imports: [RouterModule.forChild(linkRoutes)],
exports: [RouterModule]
})
export class LinkRoutingModule { }

View File

@ -0,0 +1,34 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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 { CommonModule } from '@angular/common';
import { LinkComponent } from './link/link.component';
import { SvgModule } from '../../fw/svg/svg.module';
import { LinkRoutingModule } from './link-routing.module';
import { WidgetModule } from '../../fw/widget/widget.module';
@NgModule({
imports: [
CommonModule,
LinkRoutingModule,
SvgModule,
WidgetModule
],
declarations: [
LinkComponent
]
})
export class LinkModule { }

View File

@ -0,0 +1,39 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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.
*/
#ov-link .tabular-header {
text-align: left;
}
#ov-link div.summary-list .table-header td {
font-weight: bold;
font-variant: small-caps;
text-transform: uppercase;
font-size: 10pt;
padding-top: 8px;
padding-bottom: 8px;
letter-spacing: 0.02em;
cursor: pointer;
background-color: #e5e5e6;
}
#ov-link h2 {
display: inline-block;
}
#ov-link th, td {
text-align: left;
padding: 8px;
}

View File

@ -0,0 +1,66 @@
<!--
~ Copyright 2018-present Open Networking Foundation
~
~ 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.
-->
<div id="ov-link">
<div class="tabular-header">
<h2>Links ({{tableData.length}} total)</h2>
<div class="ctrl-btns">
<div class="refresh" (click)="toggleRefresh()">
<onos-icon classes="{{ autoRefresh?'active refresh':'refresh'}}" iconId="refresh" iconSize="42" toolTip="{{ autoRefreshTip }}"></onos-icon>
</div>
</div>
</div>
<div class="summary-list" onosTableResize>
<div class="table-header">
<table>
<tr>
<td colId="available" class="table-icon"></td>
<td colId="one" (click)="onSort('one')">Port 1
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('one')"></onos-icon>
</td>
<td colId="two" (click)="onSort('two')">Port 2
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('two')"></onos-icon>
</td>
<td colId="type" (click)="onSort('type')">Type
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('type')"></onos-icon>
</td>
<td colId="direction" (click)="onSort('direction')">Direction
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('direction')"></onos-icon>
</td>
<td colId="durable" (click)="onSort('durable')">Durable
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('durable')"></onos-icon>
</td>
</tr>
</table>
</div>
<div class="table-body">
<table>
<tr *ngIf="tableData.length === 0" class="no-data">
<td colspan="6">{{ annots.noRowsMsg }}</td>
</tr>
<tr *ngFor="let link of tableData">
<td class="table-icon">
<onos-icon classes="{{link._iconid_state === 'active'? 'active':'inactive'}}" iconId="{{link._iconid_state}}"></onos-icon>
</td>
<td>{{link.one}}</td>
<td>{{link.two}}</td>
<td>{{link.type}}</td>
<td [innerHtml]="link.direction"></td>
<td>{{link.durable}}</td>
</tr>
</table>
</div>
</div>
</div>

View File

@ -0,0 +1,143 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { LinkComponent } from './link.component';
import { ActivatedRoute, Params } from '@angular/router';
import { of } from 'rxjs';
import { FnService } from '../../../fw/util/fn.service';
import { LogService } from '../../../log.service';
import { IconComponent } from '../../../fw/svg/icon/icon.component';
import { IconService } from '../../../fw/svg/icon.service';
import { LoadingService } from '../../../fw/layer/loading.service';
import { WebSocketService } from '../../../fw/remote/websocket.service';
import { DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';
class MockActivatedRoute extends ActivatedRoute {
constructor(params: Params) {
super();
this.queryParams = of(params);
}
}
class MockIconService {
loadIconDef() { }
}
class MockLoadingService {
startAnim() { }
stop() { }
waiting() { }
}
class MockWebSocketService {
createWebSocket() { }
isConnected() { return false; }
unbindHandlers() { }
bindHandlers() { }
}
/**
* ONOS GUI -- Link View Module - Unit Tests
*/
describe('LinkComponent', () => {
let fs: FnService;
let ar: MockActivatedRoute;
let windowMock: Window;
let logServiceSpy: jasmine.SpyObj<LogService>;
let component: LinkComponent;
let fixture: ComponentFixture<LinkComponent>;
beforeEach(async(() => {
const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
ar = new MockActivatedRoute({ 'debug': 'txrx' });
windowMock = <any>{
location: <any>{
linkname: 'foo',
link: 'foo',
port: '80',
protocol: 'http',
search: { debug: 'true' },
href: 'ws://foo:123/onos/ui/websock/path',
absUrl: 'ws://foo:123/onos/ui/websock/path'
}
};
fs = new FnService(ar, logSpy, windowMock);
TestBed.configureTestingModule({
declarations: [LinkComponent, IconComponent],
providers: [
{ provide: FnService, useValue: fs },
{ provide: IconService, useClass: MockIconService },
{ provide: LoadingService, useClass: MockLoadingService },
{ provide: LogService, useValue: logSpy },
{ provide: WebSocketService, useClass: MockWebSocketService },
]
})
.compileComponents();
logServiceSpy = TestBed.get(LogService);
}));
beforeEach(() => {
fixture = TestBed.createComponent(LinkComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should have a div.tabular-header inside a div#ov-link', () => {
const linkDe: DebugElement = fixture.debugElement;
const divDe = linkDe.query(By.css('div#ov-link div.tabular-header'));
expect(divDe).toBeTruthy();
});
it('should have a h2 inside the div.tabular-header', () => {
const linkDe: DebugElement = fixture.debugElement;
const divDe = linkDe.query(By.css('div#ov-link div.tabular-header h2'));
const div: HTMLElement = divDe.nativeElement;
expect(div.textContent).toEqual('Links (0 total)');
});
it('should have a refresh button inside the div.tabular-header', () => {
const linkDe: DebugElement = fixture.debugElement;
const divDe = linkDe.query(By.css('div#ov-link div.tabular-header div.ctrl-btns div.refresh'));
expect(divDe).toBeTruthy();
});
it('should have a div.summary-list inside a div#ov-link', () => {
const linkDe: DebugElement = fixture.debugElement;
const divDe = linkDe.query(By.css('div#ov-link div.summary-list'));
expect(divDe).toBeTruthy();
});
it('should have a div.table-header inside a div.summary-list inside a div#ov-link', () => {
const linkDe: DebugElement = fixture.debugElement;
const divDe = linkDe.query(By.css('div#ov-link div.summary-list div.table-header'));
expect(divDe).toBeTruthy();
});
it('should have a div.table-body inside a div.summary-list inside a div#ov-link', () => {
const linkDe: DebugElement = fixture.debugElement;
const divDe = linkDe.query(By.css('div#ov-link div.summary-list div.table-body'));
expect(divDe).toBeTruthy();
});
});

View File

@ -0,0 +1,81 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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, OnDestroy } from '@angular/core';
import { LogService } from '../../../log.service';
import { TableBaseImpl, TableResponse, SortDir } from '../../../fw/widget/table.base';
import { FnService } from '../../../fw/util/fn.service';
import { LoadingService } from '../../../fw/layer/loading.service';
import { WebSocketService } from '../../../fw/remote/websocket.service';
/**
* Model of the response from WebSocket
*/
interface LinkTableResponse extends TableResponse {
links: Link[];
}
/**
* Model of the links returned from the WebSocket
*/
interface Link {
one: string;
two: string;
type: string;
direction: string;
durable: string;
_iconid_state: string;
}
/**
* ONOS GUI -- Link View Component
*/
@Component({
selector: 'onos-link',
templateUrl: './link.component.html',
styleUrls: ['./link.component.css', '../../../fw/widget/table.css', '../../../fw/widget/table.theme.css']
})
export class LinkComponent extends TableBaseImpl implements OnInit, OnDestroy {
constructor(
protected fs: FnService,
protected ls: LoadingService,
protected log: LogService,
protected wss: WebSocketService,
) {
super(fs, ls, log, wss, 'link');
this.responseCallback = this.linkResponseCb;
this.sortParams = {
firstCol: 'one',
firstDir: SortDir.desc,
secondCol: 'two',
secondDir: SortDir.asc,
};
}
ngOnInit() {
this.init();
this.log.debug('LinkComponent initialized');
}
ngOnDestroy() {
this.destroy();
this.log.debug('LinkComponent destroyed');
}
linkResponseCb(data: LinkTableResponse) {
this.log.debug('Link response received for ', data.links.length, 'links');
}
}

View File

@ -0,0 +1,33 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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 { Routes, RouterModule } from '@angular/router';
import { MeterComponent } from './meter/meter.component';
const meterRoutes: Routes = [
{
path: '',
component: MeterComponent
}
];
@NgModule({
imports: [RouterModule.forChild(meterRoutes)],
exports: [RouterModule]
})
export class MeterRoutingModule { }

View File

@ -0,0 +1,37 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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 { CommonModule } from '@angular/common';
import { SvgModule } from '../../fw/svg/svg.module';
import { WidgetModule } from '../../fw/widget/widget.module';
import { MeterRoutingModule } from './meter-routing.module';
import { MeterComponent } from './meter/meter.component';
import { FormsModule } from '@angular/forms';
@NgModule({
imports: [
CommonModule,
SvgModule,
MeterRoutingModule,
FormsModule,
WidgetModule
],
declarations: [MeterComponent]
})
export class MeterModule { }

View File

@ -0,0 +1,58 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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.
*/
/*
ONOS GUI -- Meter View (layout) -- CSS file
*/
#ov-meter h2 {
display: inline-block;
}
#ov-meter div.ctrl-btns {
}
#ov-meter td {
text-align: center;
}
#ov-meter td.bands {
text-align: left;
}
#ov-meter td.right {
text-align: right;
}
#ov-meter .tabular-header {
text-align: left;
}
#ov-meter div.summary-list .table-header td {
font-weight: bold;
font-variant: small-caps;
text-transform: uppercase;
font-size: 10pt;
padding-top: 8px;
padding-bottom: 8px;
letter-spacing: 0.02em;
cursor: pointer;
background-color: #e5e5e6;
color: #3c3a3a;
}
#ov-meter div.summary-list td.bands {
padding-left: 36px;
}

View File

@ -0,0 +1,101 @@
<!--
~ Copyright 2018-present Open Networking Foundation
~
~ 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.
-->
<div id="ov-meter">
<div class="tabular-header">
<h2> Meter for Device {{id}} ({{tableData.length}} Total )</h2>
<div class="ctrl-btns">
<div class="refresh" (click)="toggleRefresh()">
<!-- See icon.theme.css for the defintions of the classes active and refresh-->
<onos-icon classes="{{ autoRefresh?'active refresh':'refresh' }}" iconId="refresh" iconSize="42" toolTip="{{ autoRefreshTip }}"></onos-icon>
</div>
<div class="separator"></div>
<div routerLink="/device" [queryParams]="{ devId: id }" routerLinkActive="active">
<onos-icon classes="{{ id ? 'active-rect':undefined }}" iconId="deviceTable" iconSize="42" toolTip="{{deviceTip}}"></onos-icon>
</div>
<div routerLink="/flow" [queryParams]="{ devId: id }" routerLinkActive="active">
<onos-icon classes="{{ id ? 'active-rect' :undefined}}" iconId="flowTable" iconSize="42" toolTip="{{ flowTip }}"></onos-icon>
</div>
<div routerLink="/port" [queryParams]="{ devId: id }" routerLinkActive="active">
<onos-icon classes="{{ id ? 'active-rect' :undefined}}" iconId="portTable" iconSize="42" toolTip="{{ portTip }}"></onos-icon>
</div>
<div routerLink="/group" [queryParams]="{ devId: id }" routerLinkActive="active">
<onos-icon classes="{{ id ? 'active-rect' :undefined}}" iconId="groupTable" iconSize="42" toolTip="{{ groupTip }}"></onos-icon>
</div>
<div>
<onos-icon classes="{{ id ? 'current-view' :undefined}}" iconId="meterTable" iconSize="42"></onos-icon>
</div>
<div routerLink="/pipeconf" [queryParams]="{ devId: id }" routerLinkActive="active">
<onos-icon classes="{{ id ? 'active-rect' :undefined}}" iconId="pipeconfTable" iconSize="42" toolTip="{{ pipeconfTip }}"></onos-icon>
</div>
</div>
<div class="search">
<input id="searchinput" [(ngModel)]="tableDataFilter.queryStr" type="search" #search placeholder="Search" />
<select [(ngModel)]="tableDataFilter.queryBy">
<option value="" disabled>Search By</option>
<option value="$">All Fields</option>
<option value="id">Meter ID</option>
<option value="app_id">App ID</option>
<option value="state">State</option>
</select>
</div>
</div>
<div class="summary-list" onosTableResize>
<div class="table-header">
<table>
<tr>
<td colId="id" (click)="onSort('id')">Meter ID
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('id')"></onos-icon>
</td>
<td colId="app_id" (click)="onSort('app_id')">App ID
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('app_id')"></onos-icon>
</td>
<td colId="state" (click)="onSort('state')">State
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('state')"></onos-icon>
</td>
<td colId="packets" (click)="onSort('packets')">Packets
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('packets')"></onos-icon>
</td>
<td colId="bytes" (click)="onSort('bytes')">
Bytes
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('bytes')"></onos-icon>
</td>
</tr>
</table>
</div>
<div class="table-body">
<table>
<tr class="table-body" *ngIf="tableData.length === 0" class="no-data">
<td colspan="5">{{annots.noRowsMsg}}</td>
</tr>
<ng-template ngFor let-meter [ngForOf]="tableData | filter : tableDataFilter">
<tr (click)="selectCallback($event, meter)" [ngClass]="{selected: meter.id === selId, 'data-change': isChanged(meter.id)}">
<td>{{meter.id}}</td>
<td>{{meter.app_id}}</td>
<td>{{meter.state}}</td>
<td>{{meter.packets}}</td>
<td>{{meter.bytes}}</td>
</tr>
<tr>
<td class="bands" colspan="5" [innerHTML]="meter.bands"></td>
</tr>
</ng-template>
</table>
</div>
</div>
</div>

View File

@ -0,0 +1,196 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ActivatedRoute, Params } from '@angular/router';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FormsModule } from '@angular/forms';
import { DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';
import { MeterComponent } from './meter.component';
import { LogService } from '../../../log.service';
import { DialogService } from '../../../fw/layer/dialog.service';
import { FnService } from '../../../fw/util/fn.service';
import { IconComponent } from '../../../fw/svg/icon/icon.component';
import { IconService } from '../../../fw/svg/icon.service';
import { KeyService } from '../../../fw/util/key.service';
import { LionService } from '../../../fw/util/lion.service';
import { LoadingService } from '../../../fw/layer/loading.service';
import { ThemeService } from '../../../fw/util/theme.service';
import { TableFilterPipe } from '../../../fw/widget/tablefilter.pipe';
import { UrlFnService } from '../../../fw/remote/urlfn.service';
import { WebSocketService } from '../../../fw/remote/websocket.service';
import { of, Subject } from 'rxjs';
import { } from 'jasmine';
import { RouterTestingModule } from '@angular/router/testing';
class MockActivatedRoute extends ActivatedRoute {
constructor(params: Params) {
super();
this.queryParams = of(params);
}
}
class MockDialogService { }
class MockFnService { }
class MockIconService {
loadIconDef() { }
}
class MockKeyService { }
class MockLoadingService {
startAnim() { }
stop() { }
waiting() { }
}
class MockThemeService { }
class MockUrlFnService { }
class MockWebSocketService {
createWebSocket() { }
isConnected() { return false; }
unbindHandlers() { }
bindHandlers() { }
}
/**
* ONOS GUI -- Meter Panel View -- Unit Tests
*/
describe('MeterComponent', () => {
let fs: FnService;
let ar: MockActivatedRoute;
let windowMock: Window;
let logServiceSpy: jasmine.SpyObj<LogService>;
let component: MeterComponent;
let fixture: ComponentFixture<MeterComponent>;
const bundleObj = {
'core.view.Meter': {
test: 'test1'
}
};
const mockLion = (key) => {
return bundleObj[key] || '%' + key + '%';
};
beforeEach(async(() => {
const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
ar = new MockActivatedRoute({ 'debug': 'txrx' });
windowMock = <any>{
location: <any>{
hostname: 'foo',
host: 'foo',
port: '80',
protocol: 'http',
search: { debug: 'true' },
href: 'ws://foo:123/onos/ui/websock/path',
absUrl: 'ws://foo:123/onos/ui/websock/path'
}
};
fs = new FnService(ar, logSpy, windowMock);
TestBed.configureTestingModule({
imports: [BrowserAnimationsModule, FormsModule, RouterTestingModule],
declarations: [MeterComponent, IconComponent, TableFilterPipe],
providers: [
{ provide: DialogService, useClass: MockDialogService },
{ provide: FnService, useValue: fs },
{ provide: IconService, useClass: MockIconService },
{ provide: KeyService, useClass: MockKeyService },
{
provide: LionService, useFactory: (() => {
return {
bundle: ((bundleId) => mockLion),
ubercache: new Array(),
loadCbs: new Map<string, () => void>([])
};
})
},
{ provide: LoadingService, useClass: MockLoadingService },
{ provide: LogService, useValue: logSpy },
{ provide: ThemeService, useClass: MockThemeService },
{ provide: UrlFnService, useClass: MockUrlFnService },
{ provide: WebSocketService, useClass: MockWebSocketService },
{ provide: 'Window', useValue: windowMock },
]
})
.compileComponents();
logServiceSpy = TestBed.get(LogService);
}));
beforeEach(() => {
fixture = TestBed.createComponent(MeterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should have a div.tabular-header inside a div#ov-meter', () => {
const metDe: DebugElement = fixture.debugElement;
const divDe = metDe.query(By.css('div#ov-meter div.tabular-header'));
expect(divDe).toBeTruthy();
});
it('should have a h2 inside the div.tabular-header', () => {
const metDe: DebugElement = fixture.debugElement;
const divDe = metDe.query(By.css('div#ov-meter div.tabular-header h2'));
const div: HTMLElement = divDe.nativeElement;
expect(div.textContent).toEqual(' Meter for Device (0 Total )');
});
it('should have a refresh button inside the div.tabular-header', () => {
const metDe: DebugElement = fixture.debugElement;
const divDe = metDe.query(By.css('div#ov-meter div.tabular-header div.ctrl-btns div.refresh'));
expect(divDe).toBeTruthy();
});
it('should have a div.summary-list inside a div#ov-meter', () => {
const hostDe: DebugElement = fixture.debugElement;
const divDe = hostDe.query(By.css('div#ov-meter div.summary-list'));
expect(divDe).toBeTruthy();
});
it('should have a div.table-header inside a div.summary-list inside a div#ov-meter', () => {
const hostDe: DebugElement = fixture.debugElement;
const divDe = hostDe.query(By.css('div#ov-meter div.summary-list div.table-header'));
expect(divDe).toBeTruthy();
});
it('should have a div.table-body inside a div.summary-list inside a div#ov-meter', () => {
const hostDe: DebugElement = fixture.debugElement;
const divDe = hostDe.query(By.css('div#ov-meter div.summary-list div.table-body'));
expect(divDe).toBeTruthy();
});
});

View File

@ -0,0 +1,107 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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, OnDestroy, OnInit } from '@angular/core';
import { SortDir, TableBaseImpl, TableResponse } from '../../../fw/widget/table.base';
import { WebSocketService } from '../../../fw/remote/websocket.service';
import { LogService } from '../../../log.service';
import { LoadingService } from '../../../fw/layer/loading.service';
import { FnService } from '../../../fw/util/fn.service';
import { ActivatedRoute } from '@angular/router';
/**
* Model of the response from WebSocket
*/
interface MeterTableResponse extends TableResponse {
meters: Meter[];
}
/**
* Model of the meter returned from the WebSocket
*/
interface Meter {
id: string;
appId: string;
state: string;
packets: string;
bytes: string;
}
/**
* ONOS GUI -- Meter View Component
*/
@Component({
selector: 'onos-meter',
templateUrl: './meter.component.html',
styleUrls: ['./meter.component.css', './meter.theme.css',
'../../../fw/widget/table.css', '../../../fw/widget/table.theme.css']
})
export class MeterComponent extends TableBaseImpl implements OnInit, OnDestroy {
id: string;
brief: boolean = true;
// TODO: Update for LION
deviceTip = 'Show device table';
detailTip = 'Switch to detail view';
flowTip = 'Show flow view for selected device';
portTip = 'Show port view for selected device';
groupTip = 'Show group view for selected device';
pipeconfTip = 'Show pipeconf view for selected device';
constructor(
protected fs: FnService,
protected log: LogService,
protected ls: LoadingService,
protected as: ActivatedRoute,
protected wss: WebSocketService,
) {
super(fs, ls, log, wss, 'meter');
this.as.queryParams.subscribe(params => {
this.id = params['devId'];
});
this.payloadParams = {
devId: this.id
};
this.responseCallback = this.meterResponseCb;
this.sortParams = {
firstCol: 'id',
firstDir: SortDir.desc,
secondCol: 'app_id',
secondDir: SortDir.asc,
};
}
ngOnInit() {
this.init();
this.log.debug('MeterComponent initialized');
}
ngOnDestroy() {
this.destroy();
this.log.debug('MeterComponent destroyed');
}
meterResponseCb(data: MeterTableResponse) {
this.log.debug('Meter response received for ', data.meters.length, 'meter');
}
briefToggle() {
this.brief = !this.brief;
}
}

View File

@ -0,0 +1,59 @@
/*
* Copyright 2016-present Open Networking Foundation
*
* 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.
*/
/*
ONOS GUI -- Meter View (theme) -- CSS file
*/
/* a "logical" row is made up of 2 "physical" rows -- color as such */
#ov-meter tr:nth-child(4n + 1),
#ov-meter tr:nth-child(4n + 2) {
background-color: #fbfbfb;
}
#ov-meter tr:nth-child(4n + 3),
#ov-meter tr:nth-child(4n) {
background-color: #f4f4f4;
}
/* highlighted color */
#ov-meter tr:nth-child(4n + 1).data-change,
#ov-meter tr:nth-child(4n + 2).data-change,
#ov-meter tr:nth-child(4n + 3).data-change,
#ov-meter tr:nth-child(4n).data-change {
background-color: #FDFFDC;
}
/* ========== DARK Theme ========== */
.dark #ov-meter tr:nth-child(4n + 1),
.dark #ov-meter tr:nth-child(4n + 2) {
background-color: #333333;
}
.dark #ov-meter tr:nth-child(4n + 3),
.dark #ov-meter tr:nth-child(4n) {
background-color: #3a3a3a;
}
.dark #ov-meter tr:nth-child(4n + 1).data-change,
.dark #ov-meter tr:nth-child(4n + 2).data-change,
.dark #ov-meter tr:nth-child(4n + 3).data-change,
.dark #ov-meter tr:nth-child(4n).data-change {
background-color: #423708;
}

View File

@ -0,0 +1,37 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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 { Routes, RouterModule } from '@angular/router';
import { PortComponent } from './port/port.component';
const portRoutes: Routes = [
{
path: '',
component: PortComponent
}
];
/**
* ONOS GUI -- Devices Tabular View Feature Routing Module - allows it to be lazy loaded
*
* See https://angular.io/guide/lazy-loading-ngmodules
*/
@NgModule({
imports: [RouterModule.forChild(portRoutes)],
exports: [RouterModule]
})
export class PortRoutingModule { }

View File

@ -0,0 +1,35 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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 { CommonModule } from '@angular/common';
import { PortComponent } from './port/port.component';
import { SvgModule } from '../../fw/svg/svg.module';
import { FormsModule } from '@angular/forms';
import { WidgetModule } from '../../fw/widget/widget.module';
import { PortRoutingModule } from './port-routing.module';
import { PortDetailsComponent } from './portdetails/portdetails.component';
@NgModule({
imports: [
CommonModule,
SvgModule,
PortRoutingModule,
FormsModule,
WidgetModule
],
declarations: [PortComponent, PortDetailsComponent]
})
export class PortModule { }

View File

@ -0,0 +1,59 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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.
*/
/*
ONOS GUI -- Port View (layout) -- CSS file
*/
#ov-port .tabular-header {
text-align: left;
}
#ov-port h2 {
display: inline-block;
}
#ov-port div.ctrl-btns {
}
#ov-port div.summary-list .table-header td {
font-weight: bold;
font-variant: small-caps;
text-transform: uppercase;
font-size: 10pt;
padding-top: 8px;
padding-bottom: 8px;
letter-spacing: 0.02em;
cursor: pointer;
background-color: #e5e5e6;
color: #3c3a3a;
}
#ov-port td {
text-align: center;
}
#ov-port td.delta {
text-align: center;
font-weight: bold;
}
#ov-port td.delta:before {
content: "+";
}
#ov-port tr.no-data td {
text-align: center;
}

View File

@ -0,0 +1,136 @@
<!--
~ Copyright 2018-present Open Networking Foundation
~
~ 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.
-->
<!-- Port partial HTML -->
<div id="ov-port">
<div class="tabular-header">
<h2>
Ports for Device {{devId}} ({{tableData.length}} Total)
</h2>
<div class="ctrl-btns">
<div class="refresh" (click)="toggleRefresh()">
<!-- See icon.theme.css for the defintions of the classes active and refresh-->
<onos-icon classes="{{ autoRefresh?'active refresh':'refresh' }}" iconId="refresh" iconSize="42" toolTip="{{ autoRefreshTip }}"></onos-icon>
</div>
<div class="separator"></div>
<div class="refresh" (click)="toggleNZState()">
<onos-icon classes="{{ isNz() ? 'refresh' :'active refresh'}}" iconId="nonzero" iconSize="42" toolTip="{{toggleNZTip}}">
</onos-icon>
</div>
<div class="refresh" (click)="toggleDeltaState()">
<onos-icon classes="{{ isDelta() ? 'active refresh' :'refresh'}}" iconId="delta" iconSize="42" toolTip="{{toggleDeltaTip}}"></onos-icon>
</div>
<div class="separator"></div>
<div routerLink="/device" [queryParams]="{ devId: devId }" routerLinkActive="active">
<onos-icon classes="{{ devId ? 'active-rect':undefined }}" iconId="deviceTable" iconSize="42" toolTip="{{deviceTip}}"></onos-icon>
</div>
<div routerLink="/flow" [queryParams]="{ devId: devId }" routerLinkActive="active">
<onos-icon classes="{{ devId ? 'active-rect' :undefined}}" iconId="flowTable" iconSize="42" toolTip="{{ flowTip }}"></onos-icon>
</div>
<div>
<onos-icon classes="{{ devId ? 'current-view' :undefined}}" iconId="portTable" iconSize="42"></onos-icon>
</div>
<div routerLink="/group" [queryParams]="{ devId: devId }" routerLinkActive="active">
<onos-icon classes="{{ devId ? 'active-rect' :undefined}}" iconId="groupTable" iconSize="42" toolTip="{{ groupTip }}"></onos-icon>
</div>
<div routerLink="/meter" [queryParams]="{ devId: devId }" routerLinkActive="active">
<onos-icon classes="{{ devId ? 'active-rect' :undefined}}" iconId="meterTable" iconSize="42" toolTip="{{ meterTip }}"></onos-icon>
</div>
<div routerLink="/pipeconf" [queryParams]="{ devId: devId }" routerLinkActive="active">
<onos-icon classes="{{ devId ? 'active-rect' :undefined}}" iconId="pipeconfTable" iconSize="42" toolTip="{{ pipeconfTip }}"></onos-icon>
</div>
</div>
<div class="search">
<input id="searchinput" [(ngModel)]="tableDataFilter.queryStr" type="search" #search placeholder="Search" />
<select [(ngModel)]="tableDataFilter.queryBy">
<option value="" disabled>Search By</option>
<option value="$">All Fields</option>
<option value="id">Port ID</option>
<option value="pkt_rx">Pkts Received</option>
<option value="pkt_tx">Pkts Sent</option>
<option value="bytes_rx">Bytes Received</option>
<option value="bytes_tx">Bytes Sent</option>
<option value="pkt_rx_drp">Pkts RX Dropped</option>
<option value="pkt_rx_drp">Pkts TX Dropped</option>
<option value="duration">Duration (sec)</option>
</select>
</div>
</div>
<div class="summary-list" onosTableResize>
<div class="table-header">
<table>
<tr>
<td colId="id" (click)="onSort('id')">Port ID
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('id')"></onos-icon>
</td>
<td colId="pkt_rx" (click)="onSort('pkt_rx')">Pkts Received
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('pkt_rx')"></onos-icon>
</td>
<td colId="pkt_tx" (click)="onSort('pkt_tx')">Pkts Sent
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('pkt_tx')"></onos-icon>
</td>
<td colId="bytes_rx" (click)="onSort('bytes_rx')">Bytes Received
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('bytes_rx')"></onos-icon>
</td>
<td colId="bytes_tx" (click)="onSort('bytes_tx')">Bytes Sent
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('bytes_tx')"></onos-icon>
</td>
<td colId="pkt_rx_drp" (click)="onSort('pkt_rx_drp')">Pkts RX Dropped
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('pkt_rx_drp')"></onos-icon>
</td>
<td colId="pkt_tx_drp" (click)="onSort('pkt_tx_drp')">Pkts TX Dropped
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('pkt_tx_drp')"></onos-icon>
</td>
<td colId="duration" (click)="onSort('duration')">Duration (sec)
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('duration')"></onos-icon>
</td>
</tr>
</table>
</div>
<div class="table-body">
<table>
<tr class="table-body" *ngIf="tableData.length === 0" class="no-data">
<td colspan="9">{{annots.noRowsMsg}}</td>
</tr>
<tr *ngFor="let port of tableData | filter : tableDataFilter" (click)="selectCallback($event, port)" [ngClass]="{selected: port.id === selId, 'data-change': isChanged(port.id)}">
<td>{{port.id}}</td>
<td [ngClass]="(isDelta() ? 'delta' : '')">{{port.pkt_rx}}</td>
<td [ngClass]="(isDelta() ? 'delta' : '')">{{port.pkt_tx}}</td>
<td [ngClass]="(isDelta() ? 'delta' : '')">{{port.bytes_rx}}</td>
<td [ngClass]="(isDelta() ? 'delta' : '')">{{port.bytes_tx}}</td>
<td [ngClass]="(isDelta() ? 'delta' : '')">{{port.pkt_rx_drp}}</td>
<td [ngClass]="(isDelta() ? 'delta' : '')">{{port.pkt_tx_drp}}</td>
<td [ngClass]="(isDelta() ? 'delta' : '')">{{port.duration}}</td>
</tr>
</table>
</div>
<onos-portdetails class="floatpanels" id="{{ selId }}" devId="{{devId}}" (closeEvent)="deselectRow($event)"></onos-portdetails>
</div>
</div>

View File

@ -0,0 +1,175 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { PortComponent } from './port.component';
import { ActivatedRoute, Params } from '@angular/router';
import { of } from 'rxjs/index';
import { FnService } from '../../../fw/util/fn.service';
import { LogService } from '../../../log.service';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FormsModule } from '@angular/forms';
import { RouterTestingModule } from '@angular/router/testing';
import { IconComponent } from '../../../fw/svg/icon/icon.component';
import { TableFilterPipe } from '../../../fw/widget/tablefilter.pipe';
import { GlyphService } from '../../../fw/svg/glyph.service';
import { IconService } from '../../../fw/svg/icon.service';
import { KeyService } from '../../../fw/util/key.service';
import { LoadingService } from '../../../fw/layer/loading.service';
import { MastService } from '../../../fw/mast/mast.service';
import { NavService } from '../../../fw/nav/nav.service';
import { ThemeService } from '../../../fw/util/theme.service';
import { WebSocketService } from '../../../fw/remote/websocket.service';
import { DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';
import { PortDetailsComponent } from '../portdetails/portdetails.component';
import { PrefsService } from '../../../fw/util/prefs.service';
class MockActivatedRoute extends ActivatedRoute {
constructor(params: Params) {
super();
this.queryParams = of(params);
}
}
class MockIconService {
loadIconDef() { }
}
class MockPrefsService {
setPrefs() { }
getPrefs() { }
asNumbers() { }
updatePrefs() { }
}
class MockGlyphService { }
class MockKeyService { }
class MockLoadingService {
startAnim() { }
stop() { }
}
class MockNavService { }
class MockMastService { }
class MockThemeService { }
class MockWebSocketService {
createWebSocket() { }
isConnected() { return false; }
unbindHandlers() { }
bindHandlers() { }
sendEvent() { }
}
/**
* ONOS GUI -- Flow View Module - Unit Tests
*/
describe('PortComponent', () => {
let fs: FnService;
let ar: MockActivatedRoute;
let windowMock: Window;
let logServiceSpy: jasmine.SpyObj<LogService>;
let component: PortComponent;
let fixture: ComponentFixture<PortComponent>;
beforeEach(async(() => {
const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
ar = new MockActivatedRoute({ 'debug': 'txrx' });
windowMock = <any>{
location: <any>{
hostname: 'foo',
host: 'foo',
port: '80',
protocol: 'http',
search: { debug: 'true' },
href: 'ws://foo:123/onos/ui/websock/path',
absUrl: 'ws://foo:123/onos/ui/websock/path'
}
};
fs = new FnService(ar, logSpy, windowMock);
TestBed.configureTestingModule({
imports: [BrowserAnimationsModule, FormsModule, RouterTestingModule],
declarations: [PortComponent, IconComponent, TableFilterPipe, PortDetailsComponent],
providers: [
{ provide: FnService, useValue: fs },
{ provide: IconService, useClass: MockIconService },
{ provide: GlyphService, useClass: MockGlyphService },
{ provide: KeyService, useClass: MockKeyService },
{ provide: LoadingService, useClass: MockLoadingService },
{ provide: MastService, useClass: MockMastService },
{ provide: NavService, useClass: MockNavService },
{ provide: PrefsService, useClass: MockPrefsService },
{ provide: LogService, useValue: logSpy },
{ provide: ThemeService, useClass: MockThemeService },
{ provide: WebSocketService, useClass: MockWebSocketService },
{ provide: 'Window', useValue: windowMock },
]
}).compileComponents();
logServiceSpy = TestBed.get(LogService);
}));
beforeEach(() => {
fixture = TestBed.createComponent(PortComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should have a div.tabular-header inside a div#ov-port', () => {
const portDe: DebugElement = fixture.debugElement;
const divDe = portDe.query(By.css('div#ov-port div.tabular-header'));
expect(divDe).toBeTruthy();
});
it('should have a h2 inside the div.tabular-header', () => {
const portDe: DebugElement = fixture.debugElement;
const divDe = portDe.query(By.css('div#ov-port div.tabular-header h2'));
const div: HTMLElement = divDe.nativeElement;
expect(div.textContent).toEqual(' Ports for Device (0 Total) ');
});
it('should have .table-header with "Port ID..."', () => {
const portDe: DebugElement = fixture.debugElement;
const divDe = portDe.query(By.css('div#ov-port div.table-header'));
const div: HTMLElement = divDe.nativeElement;
expect(div.textContent).toEqual(
'Port ID Pkts Received Pkts Sent Bytes Received Bytes Sent Pkts RX Dropped Pkts TX Dropped Duration (sec) ');
});
it('should have a refresh button inside the div.tabular-header', () => {
const portDe: DebugElement = fixture.debugElement;
const divDe = portDe.query(By.css('div#ov-port div.tabular-header div.ctrl-btns div.refresh'));
expect(divDe).toBeTruthy();
});
it('should have a div.table-body ', () => {
const portDe: DebugElement = fixture.debugElement;
const divDe = portDe.query(By.css('div#ov-port div.table-body'));
expect(divDe).toBeTruthy();
});
});

View File

@ -0,0 +1,182 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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, OnDestroy, OnInit } from '@angular/core';
import { SortDir, TableBaseImpl, TableResponse } from '../../../fw/widget/table.base';
import { FnService } from '../../../fw/util/fn.service';
import { LoadingService } from '../../../fw/layer/loading.service';
import { LogService } from '../../../log.service';
import { ActivatedRoute } from '@angular/router';
import { WebSocketService } from '../../../fw/remote/websocket.service';
import { PrefsService } from '../../../fw/util/prefs.service';
/**
* Model of the response from WebSocket
*/
interface PortTableResponse extends TableResponse {
ports: Port[];
}
/**
* Model of the ports returned from the WebSocket
*/
interface Port {
id: string;
pktsRecieved: string;
pktsSent: string;
byteRecieved: string;
byteSent: string;
pktsRxDropped: string;
pktsTxDropped: string;
duration: string;
}
interface FilterToggleState {
devId: string;
nzFilter: boolean;
showDelta: boolean;
}
const defaultPortPrefsState = {
nzFilter: 1,
showDelta: 0,
};
/**
* ONOS GUI -- Port View Component
*/
@Component({
selector: 'onos-port',
templateUrl: './port.component.html',
styleUrls: ['./port.component.css', '../../../fw/widget/table.css', '../../../fw/widget/table.theme.css']
})
export class PortComponent extends TableBaseImpl implements OnInit, OnDestroy {
devId: string;
nzFilter: boolean = true;
showDelta: boolean = false;
prefsState = {};
toggleState: FilterToggleState;
restorePrefsConfig; // Function
deviceTip = 'Show device table';
flowTip = 'Show flow view for this device';
groupTip = 'Show group view for this device';
meterTip = 'Show meter view for selected device';
pipeconfTip = 'Show pipeconf view for selected device';
toggleDeltaTip = 'Toggle port delta statistics';
toggleNZTip = 'Toggle non zero port statistics';
constructor(protected fs: FnService,
protected ls: LoadingService,
protected log: LogService,
protected ar: ActivatedRoute,
protected wss: WebSocketService,
protected prefs: PrefsService,
) {
super(fs, ls, log, wss, 'port');
this.ar.queryParams.subscribe(params => {
this.devId = params['devId'];
});
this.payloadParams = {
devId: this.devId
};
this.responseCallback = this.portResponseCb;
this.restorePrefsConfig = this.restoreConfigFromPrefs;
this.sortParams = {
firstCol: 'id',
firstDir: SortDir.desc,
secondCol: 'pkt_rx',
secondDir: SortDir.asc,
};
}
ngOnInit() {
this.init();
this.log.debug('PortComponent initialized');
}
ngOnDestroy() {
this.destroy();
this.log.debug('PortComponent destroyed');
}
portResponseCb(data: PortTableResponse) {
this.log.debug('Port response received for ', data.ports.length, 'port');
}
isNz(): boolean {
return this.nzFilter;
}
isDelta(): boolean {
return this.showDelta;
}
toggleNZState(b?: any) {
if (b === undefined) {
this.nzFilter = !this.nzFilter;
} else {
this.nzFilter = b;
}
this.payloadParams = this.filterToggleState();
this.updatePrefsState('nzFilter', this.nzFilter);
this.forceRefesh();
}
toggleDeltaState(b?: any) {
if (b === undefined) {
this.showDelta = !this.showDelta;
} else {
this.showDelta = b;
}
this.payloadParams = this.filterToggleState();
this.updatePrefsState('showDelta', this.showDelta);
this.forceRefesh();
}
updatePrefsState(what: any, b: any) {
this.prefsState[what] = b ? 1 : 0;
this.prefs.setPrefs('port_prefs', this.prefsState);
}
filterToggleState(): FilterToggleState {
return this.toggleState = {
devId: this.devId,
nzFilter: this.nzFilter,
showDelta: this.showDelta,
};
}
forceRefesh() {
this.requestTableData();
}
restoreConfigFromPrefs() {
this.prefsState = this.prefs.asNumbers(
this.prefs.getPrefs('port_prefs', defaultPortPrefsState, )
);
this.log.debug('Port - Prefs State:', this.prefsState);
this.toggleDeltaState(this.prefsState['showDelta']);
this.toggleNZState(this.prefsState['nzFilter']);
}
}

View File

@ -0,0 +1,90 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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.
*/
#port-details-panel.floatpanel {
z-index: 0;
padding-top: 10px;
font-size: 10pt;
top: 185px;
}
#port-details-panel .container {
padding: 8px 12px;
}
#port-details-panel .close-btn {
position: absolute;
right: 5px;
top: 5px;
cursor: pointer;
}
#port-details-panel .port-icon {
display: inline-block;
padding: 0 6px 0 0;
vertical-align: middle;
}
#port-details-panel h2 {
display: inline-block;
margin: 8px 0;
font-weight: bold;
font-size: 16pt;
}
#port-details-panel h2 input {
font-size: 0.90em;
width: 106%;
}
#port-details-panel .actionBtns div {
padding: 12px 6px;
}
#port-details-panel hr {
clear: both;
width: 100%;
margin: 2px auto;
}
#port-details-panel .top-tables {
font-size: 10pt;
white-space: nowrap;
}
#port-details-panel td.label {
font-weight: bold;
text-align: right;
padding-right: 6px;
}
#port-details-panel .bottom {
border-spacing: 0;
height: 400px;
width: 520px;
overflow: auto;
display: block;
}
#port-details-panel .top div.left {
float: left;
text-align: left;
padding: 0 10px 0 0;
}
#port-details-panel .top div.right {
display: inline-block;
}

View File

@ -0,0 +1,60 @@
<!--
~ Copyright 2015-present Open Networking Foundation
~
~ 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.
-->
<div id="port-details-panel" class="floatpanel" [@portDetailsState]="id!=='' && !closed">
<div class="container">
<div class="top">
<div class="close-btn">
<onos-icon class="close-btn" classes="active-close" iconId="close" iconSize="20" (click)="close()"></onos-icon>
</div>
<div class="port-icon">
<onos-icon classes="details-icon" iconId="m_ports" [iconSize]="42"></onos-icon>
</div>
<h2>{{detailsData.devId}} port {{detailsData.id}}</h2>
<div class="top-content">
<div class="top-tables">
<div class="left">
<table>
<tbody>
<tr>
<td class="label" width="110">ID :</td>
<td class="value" width="80">{{detailsData.id}}</td>
</tr>
<tr>
<td class="label" width="110">Device :</td>
<td class="value" width="80">{{detailsData.devId}}</td>
</tr>
<tr>
<td class="label" width="110">Type :</td>
<td class="value" width="80">{{detailsData.type}}</td>
</tr>
<tr>
<td class="label" width="110">Speed :</td>
<td class="value" width="80">{{detailsData.speed}}</td>
</tr>
<tr>
<td class="label" width="110">Enabled :</td>
<td class="value" width="80">{{detailsData.enabled}}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<hr>
</div>
<div class="bottom"></div>
</div>
</div>

View File

@ -0,0 +1,123 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { PortDetailsComponent } from './portdetails.component';
import { ActivatedRoute, Params } from '@angular/router';
import { of } from 'rxjs/index';
import { LogService } from '../../../log.service';
import { FnService } from '../../../fw/util/fn.service';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { IconComponent } from '../../../fw/svg/icon/icon.component';
import { IconService } from '../../../fw/svg/icon.service';
import { WebSocketService } from '../../../fw/remote/websocket.service';
import { DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';
class MockActivatedRoute extends ActivatedRoute {
constructor(params: Params) {
super();
this.queryParams = of(params);
}
}
class MockIconService {
classes = 'active-close';
loadIconDef() { }
}
class MockWebSocketService {
createWebSocket() { }
isConnected() { return false; }
unbindHandlers() { }
bindHandlers() { }
}
describe('PortdetailsComponent', () => {
let fs: FnService;
let ar: MockActivatedRoute;
let windowMock: Window;
let logServiceSpy: jasmine.SpyObj<LogService>;
let component: PortDetailsComponent;
let fixture: ComponentFixture<PortDetailsComponent>;
beforeEach(async(() => {
const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
ar = new MockActivatedRoute({ 'debug': 'panel' });
windowMock = <any>{
location: <any>{
hostname: 'foo',
host: 'foo',
port: '80',
protocol: 'http',
search: { debug: 'true' },
href: 'ws://foo:123/onos/ui/websock/path',
absUrl: 'ws://foo:123/onos/ui/websock/path'
}
};
fs = new FnService(ar, logSpy, windowMock);
TestBed.configureTestingModule({
imports: [BrowserAnimationsModule],
declarations: [PortDetailsComponent, IconComponent],
providers: [
{ provide: FnService, useValue: fs },
{ provide: IconService, useClass: MockIconService },
{ provide: LogService, useValue: logSpy },
{ provide: WebSocketService, useClass: MockWebSocketService },
{ provide: 'Window', useValue: windowMock },
]
}).compileComponents();
logServiceSpy = TestBed.get(LogService);
}));
beforeEach(() => {
fixture = TestBed.createComponent(PortDetailsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should have an div.close-btn div.top inside a div.container', () => {
const portDe: DebugElement = fixture.debugElement;
const divDe = portDe.query(By.css('div.container div.top div.close-btn'));
expect(divDe).toBeTruthy();
});
it('should have a div.port-icon inside a div.top inside a div.container', () => {
const portDe: DebugElement = fixture.debugElement;
const divDe = portDe.query(By.css('div.container div.top div.port-icon'));
const div: HTMLElement = divDe.nativeElement;
expect(div.textContent).toEqual('');
});
it('should have a div.top-content inside a div.top inside a div.container', () => {
const portDe: DebugElement = fixture.debugElement;
const divDe = portDe.query(By.css('div.container div.top div.top-content'));
expect(divDe).toBeTruthy();
});
it('should have a dev.left inside a div.top-tables inside a div.top-content', () => {
const portDe: DebugElement = fixture.debugElement;
const divDe = portDe.query(By.css('div.top-content div.top-tables div.left'));
const div: HTMLElement = divDe.nativeElement;
expect(div.textContent).toEqual('ID :Device :Type :Speed :Enabled :');
});
});

View File

@ -0,0 +1,99 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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, Input, OnChanges, OnDestroy, OnInit } from '@angular/core';
import { animate, state, style, transition, trigger } from '@angular/animations';
import { DetailsPanelBaseImpl } from '../../../fw/widget/detailspanel.base';
import { FnService } from '../../../fw/util/fn.service';
import { LoadingService } from '../../../fw/layer/loading.service';
import { LogService } from '../../../log.service';
import { IconService } from '../../../fw/svg/icon.service';
import { WebSocketService } from '../../../fw/remote/websocket.service';
/**
* The details view when a port row is clicked from the Port view
*
* This is expected to be passed an 'id' and it makes a call
* to the WebSocket with an portDetailsRequest, and gets back an
* portDetailsResponse.
*
* The animated fly-in is controlled by the animation below
* The portDetailsState is attached to port-details-panel
* and is false (flies out) when id='' and true (flies in) when
* id has a value
*/
@Component({
selector: 'onos-portdetails',
templateUrl: './portdetails.component.html',
styleUrls: ['./portdetails.component.css', '../../../fw/widget/panel.css', '../../../fw/widget/panel-theme.css'],
animations: [
trigger('portDetailsState', [
state('true', style({
transform: 'translateX(-100%)',
opacity: '100'
})),
state('false', style({
transform: 'translateX(0%)',
opacity: '0'
})),
transition('0 => 1', animate('100ms ease-in')),
transition('1 => 0', animate('100ms ease-out'))
])
]
})
export class PortDetailsComponent extends DetailsPanelBaseImpl implements OnInit, OnDestroy, OnChanges {
@Input() id: string;
@Input() devId: string;
constructor(protected fs: FnService,
protected ls: LoadingService,
protected log: LogService,
protected is: IconService,
protected wss: WebSocketService
) {
super(fs, ls, log, wss, 'port');
}
ngOnInit() {
this.init();
this.log.debug('App Details Component initialized:', this.id);
}
/**
* Stop listening to appDetailsResponse on WebSocket
*/
ngOnDestroy() {
this.destroy();
this.log.debug('App Details Component destroyed');
}
/**
* Details Panel Data Request on row selection changes
* Should be called whenever id changes
* If id or devId is empty, no request is made
*/
ngOnChanges() {
if (this.id === '' || this.devId === '') {
return '';
} else {
const query = {
'id': this.id,
'devId': this.devId
};
this.requestDetailsPanelData(query);
}
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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 { Routes, RouterModule } from '@angular/router';
import { TunnelComponent } from './tunnel/tunnel.component';
const routes: Routes = [
{
path: '',
component: TunnelComponent
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class TunnelRoutingModule { }

View File

@ -0,0 +1,28 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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 { TunnelModule } from './tunnel.module';
describe('TunnelModule', () => {
let tunnelModule: TunnelModule;
beforeEach(() => {
tunnelModule = new TunnelModule();
});
it('should create an instance', () => {
expect(tunnelModule).toBeTruthy();
});
});

View File

@ -0,0 +1,35 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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 { CommonModule } from '@angular/common';
import { TunnelRoutingModule } from './tunnel-routing.module';
import { TunnelComponent } from './tunnel/tunnel.component';
import { SvgModule } from '../../fw/svg/svg.module';
import { WidgetModule } from '../../fw/widget/widget.module';
@NgModule({
imports: [
CommonModule,
TunnelRoutingModule,
SvgModule,
WidgetModule
],
declarations: [
TunnelComponent
]
})
export class TunnelModule { }

View File

@ -0,0 +1,39 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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.
*/
#ov-tunnel .tabular-header {
text-align: left;
}
#ov-tunnel div.summary-list .table-header td {
font-weight: bold;
font-variant: small-caps;
text-transform: uppercase;
font-size: 10pt;
padding-top: 8px;
padding-bottom: 8px;
letter-spacing: 0.02em;
cursor: pointer;
background-color: #e5e5e6;
}
#ov-tunnel h2 {
display: inline-block;
}
#ov-tunnel th, td {
text-align: left;
padding: 8px;
}

View File

@ -0,0 +1,74 @@
<!--
~ Copyright 2018-present Open Networking Foundation
~
~ 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.
-->
<div id="ov-tunnel">
<div class="tabular-header">
<h2>Tunnels ({{tableData.length}} total)</h2>
<div class="ctrl-btns">
<div class="refresh" (click)="toggleRefresh()">
<onos-icon classes="{{ autoRefresh?'active refresh':'refresh'}}" iconId="refresh" iconSize="42" toolTip="{{ autoRefreshTip }}"></onos-icon>
</div>
</div>
</div>
<div class="summary-list" onosTableResize>
<div class="table-header">
<table>
<tr>
<td colId="id" (click)="onSort('id')">Id
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('id')"></onos-icon>
</td>
<td colId="name" (click)="onSort('name')">Name
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('name')"></onos-icon>
</td>
<td colId="port1" (click)="onSort('port1')">Port 1
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('port1')"></onos-icon>
</td>
<td colId="port2" (click)="onSort('port2')">Port 2
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('port2')"></onos-icon>
</td>
<td colId="type" (click)="onSort('type')">Type
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('type')"></onos-icon>
</td>
<td colId="groupId" (click)="onSort('groupId')">Group Id
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('groupId')"></onos-icon>
</td>
<td colId="bandwidth" (click)="onSort('bandwidth')">Bandwidth
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('bandwidth')"></onos-icon>
</td>
<td colId="path" (click)="onSort('path')">Path
<onos-icon classes="active-sort" [iconSize]="10" [iconId]="sortIcon('path')"></onos-icon>
</td>
</tr>
</table>
</div>
<div class="table-body">
<table>
<tr *ngIf="tableData.length === 0" class="no-data">
<td colspan="8">{{ annots.noRowsMsg }}</td>
</tr>
<tr *ngFor="let tunnel of tableData">
<td>{{tunnel.id}}</td>
<td>{{tunnel.name}}</td>
<td>{{tunnel.one}}</td>
<td>{{tunnel.two}}</td>
<td>{{tunnel.type}}</td>
<td>{{tunnel.group_id}}</td>
<td>{{tunnel.bandwidth}}</td>
<td>{{tunnel.path}}</td>
</tr>
</table>
</div>
</div>
</div>

View File

@ -0,0 +1,143 @@
/*
* Copyright 2018-present Open Networking Foundation
*
* 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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TunnelComponent } from './tunnel.component';
import { ActivatedRoute, Params } from '@angular/router';
import { of } from 'rxjs';
import { FnService } from '../../../fw/util/fn.service';
import { LogService } from '../../../log.service';
import { LoadingService } from '../../../fw/layer/loading.service';
import { WebSocketService } from '../../../fw/remote/websocket.service';
import { IconService } from '../../../fw/svg/icon.service';
import { IconComponent } from '../../../fw/svg/icon/icon.component';
import { DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';
class MockActivatedRoute extends ActivatedRoute {
constructor(params: Params) {
super();
this.queryParams = of(params);
}
}
class MockIconService {
loadIconDef() { }
}
class MockLoadingService {
startAnim() { }
stop() { }
waiting() { }
}
class MockWebSocketService {
createWebSocket() { }
isConnected() { return false; }
unbindHandlers() { }
bindHandlers() { }
}
/**
* ONOS GUI -- Tunnel View Module - Unit Tests
*/
describe('TunnelComponent', () => {
let fs: FnService;
let ar: MockActivatedRoute;
let windowMock: Window;
let logServiceSpy: jasmine.SpyObj<LogService>;
let component: TunnelComponent;
let fixture: ComponentFixture<TunnelComponent>;
beforeEach(async(() => {
const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
ar = new MockActivatedRoute({ 'debug': 'txrx' });
windowMock = <any>{
location: <any>{
tunnelname: 'foo',
tunnel: 'foo',
port: '80',
protocol: 'http',
search: { debug: 'true' },
href: 'ws://foo:123/onos/ui/websock/path',
absUrl: 'ws://foo:123/onos/ui/websock/path'
}
};
fs = new FnService(ar, logSpy, windowMock);
TestBed.configureTestingModule({
declarations: [TunnelComponent, IconComponent],
providers: [
{ provide: FnService, useValue: fs },
{ provide: IconService, useClass: MockIconService },
{ provide: LoadingService, useClass: MockLoadingService },
{ provide: LogService, useValue: logSpy },
{ provide: WebSocketService, useClass: MockWebSocketService },
]
})
.compileComponents();
logServiceSpy = TestBed.get(LogService);
}));
beforeEach(() => {
fixture = TestBed.createComponent(TunnelComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should have a div.tabular-header inside a div#ov-tunnel', () => {
const tunnelDe: DebugElement = fixture.debugElement;
const divDe = tunnelDe.query(By.css('div#ov-tunnel div.tabular-header'));
expect(divDe).toBeTruthy();
});
it('should have a h2 inside the div.tabular-header', () => {
const tunnelDe: DebugElement = fixture.debugElement;
const divDe = tunnelDe.query(By.css('div#ov-tunnel div.tabular-header h2'));
const div: HTMLElement = divDe.nativeElement;
expect(div.textContent).toEqual('Tunnels (0 total)');
});
it('should have a refresh button inside the div.tabular-header', () => {
const tunnelDe: DebugElement = fixture.debugElement;
const divDe = tunnelDe.query(By.css('div#ov-tunnel div.tabular-header div.ctrl-btns div.refresh'));
expect(divDe).toBeTruthy();
});
it('should have a div.summary-list inside a div#ov-tunnel', () => {
const tunnelDe: DebugElement = fixture.debugElement;
const divDe = tunnelDe.query(By.css('div#ov-tunnel div.summary-list'));
expect(divDe).toBeTruthy();
});
it('should have a div.table-header inside a div.summary-list inside a div#ov-tunnel', () => {
const tunnelDe: DebugElement = fixture.debugElement;
const divDe = tunnelDe.query(By.css('div#ov-tunnel div.summary-list div.table-header'));
expect(divDe).toBeTruthy();
});
it('should have a div.table-body inside a div.summary-list inside a div#ov-tunnel', () => {
const tunnelDe: DebugElement = fixture.debugElement;
const divDe = tunnelDe.query(By.css('div#ov-tunnel div.summary-list div.table-body'));
expect(divDe).toBeTruthy();
});
});

Some files were not shown because too many files have changed in this diff Show More