mirror of
https://github.com/opennetworkinglab/onos.git
synced 2025-10-17 18:32:28 +02:00
GUI2 implementation of device/flow/port/group/meter/host/link/tunnel view
Review comments incorporated. Change-Id: I45dd6570961cc3e0f4ffddb7acbf02cd7d860de5
This commit is contained in:
parent
e910e406ba
commit
72ead49e48
@ -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
|
@ -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';
|
||||
|
@ -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>
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -40,6 +40,8 @@ export const glyphMapping = new Map<string, string>([
|
||||
['nonzero', 'nonzero'],
|
||||
['close', 'xClose'],
|
||||
|
||||
['m_ports', 'm_ports'],
|
||||
|
||||
['topo', 'topo'],
|
||||
|
||||
['refresh', 'refresh'],
|
||||
|
@ -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;;
|
||||
}
|
||||
|
||||
|
||||
|
@ -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 |
@ -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;;
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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 {
|
||||
|
||||
|
@ -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';
|
||||
|
||||
/**
|
||||
|
@ -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; });
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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');
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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');
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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'
|
||||
}
|
||||
];
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}));
|
||||
|
||||
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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';
|
||||
|
@ -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 {
|
||||
|
@ -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 = [
|
||||
{
|
||||
|
@ -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>
|
@ -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 { }
|
||||
|
@ -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.
|
@ -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>
|
@ -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();
|
||||
});
|
||||
});
|
@ -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 } });
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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>
|
@ -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();
|
||||
});
|
||||
});
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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');
|
||||
}
|
||||
|
||||
}
|
@ -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 { }
|
41
web/gui2/src/main/webapp/app/view/flow/flow.module.ts
Normal file
41
web/gui2/src/main/webapp/app/view/flow/flow.module.ts
Normal 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 { }
|
102
web/gui2/src/main/webapp/app/view/flow/flow/flow.component.css
Normal file
102
web/gui2/src/main/webapp/app/view/flow/flow/flow.component.css
Normal 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;
|
||||
}
|
136
web/gui2/src/main/webapp/app/view/flow/flow/flow.component.html
Normal file
136
web/gui2/src/main/webapp/app/view/flow/flow/flow.component.html
Normal 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>
|
@ -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();
|
||||
});
|
||||
});
|
151
web/gui2/src/main/webapp/app/view/flow/flow/flow.component.ts
Normal file
151
web/gui2/src/main/webapp/app/view/flow/flow/flow.component.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
80
web/gui2/src/main/webapp/app/view/flow/flow/flow.theme.css
Normal file
80
web/gui2/src/main/webapp/app/view/flow/flow/flow.theme.css
Normal 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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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>
|
@ -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();
|
||||
});
|
||||
});
|
@ -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);
|
||||
}
|
||||
}
|
@ -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 { }
|
38
web/gui2/src/main/webapp/app/view/group/group.module.ts
Normal file
38
web/gui2/src/main/webapp/app/view/group/group.module.ts
Normal 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 { }
|
@ -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;
|
||||
}
|
@ -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>
|
@ -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();
|
||||
});
|
||||
});
|
109
web/gui2/src/main/webapp/app/view/group/group/group.component.ts
Normal file
109
web/gui2/src/main/webapp/app/view/group/group/group.component.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
@ -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 { }
|
34
web/gui2/src/main/webapp/app/view/host/host.module.ts
Normal file
34
web/gui2/src/main/webapp/app/view/host/host.module.ts
Normal 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 { }
|
@ -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;
|
||||
}
|
@ -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>
|
@ -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();
|
||||
});
|
||||
|
||||
});
|
@ -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');
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
@ -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>
|
@ -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();
|
||||
});
|
||||
});
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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 { }
|
34
web/gui2/src/main/webapp/app/view/link/link.module.ts
Normal file
34
web/gui2/src/main/webapp/app/view/link/link.module.ts
Normal 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 { }
|
@ -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;
|
||||
}
|
@ -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>
|
@ -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();
|
||||
});
|
||||
});
|
@ -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');
|
||||
}
|
||||
}
|
@ -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 { }
|
37
web/gui2/src/main/webapp/app/view/meter/meter.module.ts
Normal file
37
web/gui2/src/main/webapp/app/view/meter/meter.module.ts
Normal 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 { }
|
@ -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;
|
||||
}
|
@ -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>
|
@ -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();
|
||||
});
|
||||
});
|
107
web/gui2/src/main/webapp/app/view/meter/meter/meter.component.ts
Normal file
107
web/gui2/src/main/webapp/app/view/meter/meter/meter.component.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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 { }
|
35
web/gui2/src/main/webapp/app/view/port/port.module.ts
Normal file
35
web/gui2/src/main/webapp/app/view/port/port.module.ts
Normal 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 { }
|
@ -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;
|
||||
}
|
136
web/gui2/src/main/webapp/app/view/port/port/port.component.html
Normal file
136
web/gui2/src/main/webapp/app/view/port/port/port.component.html
Normal 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>
|
@ -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();
|
||||
});
|
||||
});
|
182
web/gui2/src/main/webapp/app/view/port/port/port.component.ts
Normal file
182
web/gui2/src/main/webapp/app/view/port/port/port.component.ts
Normal 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']);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
@ -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>
|
@ -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 :');
|
||||
});
|
||||
});
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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 { }
|
@ -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();
|
||||
});
|
||||
});
|
35
web/gui2/src/main/webapp/app/view/tunnel/tunnel.module.ts
Normal file
35
web/gui2/src/main/webapp/app/view/tunnel/tunnel.module.ts
Normal 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 { }
|
@ -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;
|
||||
}
|
@ -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>
|
@ -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
Loading…
x
Reference in New Issue
Block a user