GUI2 implementation of device/flow/port/group/meter/host/link/tunnel view
Review comments incorporated.
Change-Id: I45dd6570961cc3e0f4ffddb7acbf02cd7d860de5
diff --git a/web/gui2/src/main/webapp/app/README.txt b/web/gui2/src/main/webapp/app/README.txt
index cffc8a1..820f36e 100644
--- a/web/gui2/src/main/webapp/app/README.txt
+++ b/web/gui2/src/main/webapp/app/README.txt
@@ -4,4 +4,20 @@
fw/ contains framework related code
-view/ contains view related code
\ No newline at end of file
+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
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.spec.ts b/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.spec.ts
index caa52ec..79e4153 100644
--- a/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.spec.ts
+++ b/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.spec.ts
@@ -22,7 +22,7 @@
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';
diff --git a/web/gui2/src/main/webapp/app/fw/nav/nav/nav.component.html b/web/gui2/src/main/webapp/app/fw/nav/nav/nav.component.html
index cb48492..f807c9a 100644
--- a/web/gui2/src/main/webapp/app/fw/nav/nav/nav.component.html
+++ b/web/gui2/src/main/webapp/app/fw/nav/nav/nav.component.html
@@ -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>
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/fw/nav/nav/nav.component.spec.ts b/web/gui2/src/main/webapp/app/fw/nav/nav/nav.component.spec.ts
index e2bd999..7079b15 100644
--- a/web/gui2/src/main/webapp/app/fw/nav/nav/nav.component.spec.ts
+++ b/web/gui2/src/main/webapp/app/fw/nav/nav/nav.component.spec.ts
@@ -21,7 +21,7 @@
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 @@
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');
});
});
diff --git a/web/gui2/src/main/webapp/app/fw/svg/glyph.service.ts b/web/gui2/src/main/webapp/app/fw/svg/glyph.service.ts
index 7e8adb4..0c1eff3 100644
--- a/web/gui2/src/main/webapp/app/fw/svg/glyph.service.ts
+++ b/web/gui2/src/main/webapp/app/fw/svg/glyph.service.ts
@@ -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 @@
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 @@
}
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 @@
}
}
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);
+ }
}
diff --git a/web/gui2/src/main/webapp/app/fw/svg/icon.service.ts b/web/gui2/src/main/webapp/app/fw/svg/icon.service.ts
index 813589f..de3672c 100644
--- a/web/gui2/src/main/webapp/app/fw/svg/icon.service.ts
+++ b/web/gui2/src/main/webapp/app/fw/svg/icon.service.ts
@@ -40,6 +40,8 @@
['nonzero', 'nonzero'],
['close', 'xClose'],
+ ['m_ports', 'm_ports'],
+
['topo', 'topo'],
['refresh', 'refresh'],
diff --git a/web/gui2/src/main/webapp/app/fw/svg/icon/icon.component.css b/web/gui2/src/main/webapp/app/fw/svg/icon/icon.component.css
index 347837a..d2d6f56 100644
--- a/web/gui2/src/main/webapp/app/fw/svg/icon/icon.component.css
+++ b/web/gui2/src/main/webapp/app/fw/svg/icon/icon.component.css
@@ -30,16 +30,58 @@
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;;
+}
+
+
diff --git a/web/gui2/src/main/webapp/app/fw/svg/icon/icon.component.html b/web/gui2/src/main/webapp/app/fw/svg/icon/icon.component.html
index 5be4c19..2243dc2 100644
--- a/web/gui2/src/main/webapp/app/fw/svg/icon/icon.component.html
+++ b/web/gui2/src/main/webapp/app/fw/svg/icon/icon.component.html
@@ -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>
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/fw/svg/icon/icon.theme.css b/web/gui2/src/main/webapp/app/fw/svg/icon/icon.theme.css
index 3e6f601..ca6e32e 100644
--- a/web/gui2/src/main/webapp/app/fw/svg/icon/icon.theme.css
+++ b/web/gui2/src/main/webapp/app/fw/svg/icon/icon.theme.css
@@ -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 @@
/* 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 @@
/* ========== 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:hover g.icon .glyph {
fill: #20b2ff;
}
-*/
\ No newline at end of file
+*/
+
+svg.embeddedIcon g.icon.devIcon_SWITCH .glyph {
+ fill: #0071bd;;
+}
+
+svg.embeddedIcon g.icon.hostIcon_endstation .glyph {
+ fill: #0071bd;;
+}
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/fw/svg/icon/tooltip.css b/web/gui2/src/main/webapp/app/fw/svg/icon/tooltip.css
index 74a5443..890f1f8 100644
--- a/web/gui2/src/main/webapp/app/fw/svg/icon/tooltip.css
+++ b/web/gui2/src/main/webapp/app/fw/svg/icon/tooltip.css
@@ -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;
}
diff --git a/web/gui2/src/main/webapp/app/fw/svg/svgutil.service.ts b/web/gui2/src/main/webapp/app/fw/svg/svgutil.service.ts
index 9cba079..23759ab 100644
--- a/web/gui2/src/main/webapp/app/fw/svg/svgutil.service.ts
+++ b/web/gui2/src/main/webapp/app/fw/svg/svgutil.service.ts
@@ -23,7 +23,7 @@
* The SVG Util Service provides a miscellany of utility functions.
*/
@Injectable({
- providedIn: 'root',
+ providedIn: 'root',
})
export class SvgUtilService {
diff --git a/web/gui2/src/main/webapp/app/fw/util/key.service.ts b/web/gui2/src/main/webapp/app/fw/util/key.service.ts
index 1eaa895..4c6f492 100644
--- a/web/gui2/src/main/webapp/app/fw/util/key.service.ts
+++ b/web/gui2/src/main/webapp/app/fw/util/key.service.ts
@@ -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';
/**
diff --git a/web/gui2/src/main/webapp/app/fw/util/prefs.service.ts b/web/gui2/src/main/webapp/app/fw/util/prefs.service.ts
index f696125..991df7f 100644
--- a/web/gui2/src/main/webapp/app/fw/util/prefs.service.ts
+++ b/web/gui2/src/main/webapp/app/fw/util/prefs.service.ts
@@ -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 @@
* 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; });
+ }
+
}
diff --git a/web/gui2/src/main/webapp/app/fw/util/random.service.ts b/web/gui2/src/main/webapp/app/fw/util/random.service.ts
index d808e48..1b6e466 100644
--- a/web/gui2/src/main/webapp/app/fw/util/random.service.ts
+++ b/web/gui2/src/main/webapp/app/fw/util/random.service.ts
@@ -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 @@
export class RandomService {
constructor(
- private fs: FnService,
private log: LogService
) {
this.log.debug('RandomService constructed');
diff --git a/web/gui2/src/main/webapp/app/fw/widget/detailspanel.base.ts b/web/gui2/src/main/webapp/app/fw/widget/detailspanel.base.ts
index d7cf7b8..59f85c5 100644
--- a/web/gui2/src/main/webapp/app/fw/widget/detailspanel.base.ts
+++ b/web/gui2/src/main/webapp/app/fw/widget/detailspanel.base.ts
@@ -19,6 +19,7 @@
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 @@
*/
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 @@
}
/**
- * 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 @@
*/
close(): void {
this.closed = true;
+ this.id = null;
+ this.closeEvent.emit(this.id);
}
+
}
diff --git a/web/gui2/src/main/webapp/app/fw/widget/panel.css b/web/gui2/src/main/webapp/app/fw/widget/panel.css
index 34d127f..48530ac 100644
--- a/web/gui2/src/main/webapp/app/fw/widget/panel.css
+++ b/web/gui2/src/main/webapp/app/fw/widget/panel.css
@@ -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;
diff --git a/web/gui2/src/main/webapp/app/fw/widget/table.base.ts b/web/gui2/src/main/webapp/app/fw/widget/table.base.ts
index 0093f72..cc29878 100644
--- a/web/gui2/src/main/webapp/app/fw/widget/table.base.ts
+++ b/web/gui2/src/main/webapp/app/fw/widget/table.base.ts
@@ -67,6 +67,11 @@
secondDir: SortDir;
}
+export interface PayloadParams {
+ devId: string;
+}
+
+
/**
* ONOS GUI -- Widget -- Table Base class
*/
@@ -74,7 +79,7 @@
// 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 @@
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 @@
return '';
}
}
+
+ /**
+ * De-selects the row
+ */
+ deselectRow(event) {
+ this.log.debug('Details panel close event');
+ this.selId = event;
+ }
}
diff --git a/web/gui2/src/main/webapp/app/fw/widget/table.css b/web/gui2/src/main/webapp/app/fw/widget/table.css
index 9df99ef..1ed43b4 100644
--- a/web/gui2/src/main/webapp/app/fw/widget/table.css
+++ b/web/gui2/src/main/webapp/app/fw/widget/table.css
@@ -31,14 +31,13 @@
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;
}
diff --git a/web/gui2/src/main/webapp/app/fw/widget/tableresize.directive.ts b/web/gui2/src/main/webapp/app/fw/widget/tableresize.directive.ts
index fef5123..21ff59c 100644
--- a/web/gui2/src/main/webapp/app/fw/widget/tableresize.directive.ts
+++ b/web/gui2/src/main/webapp/app/fw/widget/tableresize.directive.ts
@@ -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 @@
@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');
+ }
+
}
diff --git a/web/gui2/src/main/webapp/app/onos-routing.module.ts b/web/gui2/src/main/webapp/app/onos-routing.module.ts
index 5d4af2b..040139b 100644
--- a/web/gui2/src/main/webapp/app/onos-routing.module.ts
+++ b/web/gui2/src/main/webapp/app/onos-routing.module.ts
@@ -22,16 +22,44 @@
*/
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'
}
];
diff --git a/web/gui2/src/main/webapp/app/view/apps/apps.module.ts b/web/gui2/src/main/webapp/app/view/apps/apps.module.ts
index 738ee18..0e79a2b 100644
--- a/web/gui2/src/main/webapp/app/view/apps/apps.module.ts
+++ b/web/gui2/src/main/webapp/app/view/apps/apps.module.ts
@@ -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.
diff --git a/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.css b/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.css
index 3096bae..636fb0c 100644
--- a/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.css
+++ b/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.css
@@ -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.
diff --git a/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.html b/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.html
index 943e26b..37826fa 100644
--- a/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.html
+++ b/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.html
@@ -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>
diff --git a/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.spec.ts b/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.spec.ts
index cddf9ae..e022ebc 100644
--- a/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.spec.ts
+++ b/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.spec.ts
@@ -38,6 +38,7 @@
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 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 @@
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 @@
{ 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 @@
{ provide: 'Window', useValue: windowMock },
]
})
- .compileComponents();
+ .compileComponents();
logServiceSpy = TestBed.get(LogService);
}));
diff --git a/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.ts b/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.ts
index ffd7b37..b2f6231 100644
--- a/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.ts
+++ b/web/gui2/src/main/webapp/app/view/apps/apps/apps.component.ts
@@ -337,4 +337,13 @@
evt.preventDefault();
evt.stopPropagation();
}
+
+ deselectRow(event) {
+ this.log.debug('Details panel close event');
+ this.selId = event;
+ this.ctrlBtnState = <CtrlBtnState>{
+ installed: undefined,
+ active: undefined
+ };
+ }
}
diff --git a/web/gui2/src/main/webapp/app/view/apps/apps/apps.theme.css b/web/gui2/src/main/webapp/app/view/apps/apps/apps.theme.css
index 13f1847..a705d45 100644
--- a/web/gui2/src/main/webapp/app/view/apps/apps/apps.theme.css
+++ b/web/gui2/src/main/webapp/app/view/apps/apps/apps.theme.css
@@ -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 @@
#ov-app div.summary-list .table-body {
overflow:scroll;
- max-height:70vh;
}
#ov-app h2 {
display: inline-block;
diff --git a/web/gui2/src/main/webapp/app/view/apps/appsdetails/appsdetails.component.css b/web/gui2/src/main/webapp/app/view/apps/appsdetails/appsdetails.component.css
index ce9af3a..90d4b14 100644
--- a/web/gui2/src/main/webapp/app/view/apps/appsdetails/appsdetails.component.css
+++ b/web/gui2/src/main/webapp/app/view/apps/appsdetails/appsdetails.component.css
@@ -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.
diff --git a/web/gui2/src/main/webapp/app/view/apps/appsdetails/appsdetails.component.html b/web/gui2/src/main/webapp/app/view/apps/appsdetails/appsdetails.component.html
index a78bdd9..d43c264 100644
--- a/web/gui2/src/main/webapp/app/view/apps/appsdetails/appsdetails.component.html
+++ b/web/gui2/src/main/webapp/app/view/apps/appsdetails/appsdetails.component.html
@@ -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.
diff --git a/web/gui2/src/main/webapp/app/view/apps/appsdetails/appsdetails.component.spec.ts b/web/gui2/src/main/webapp/app/view/apps/appsdetails/appsdetails.component.spec.ts
index f890cde..75f5f8e 100644
--- a/web/gui2/src/main/webapp/app/view/apps/appsdetails/appsdetails.component.spec.ts
+++ b/web/gui2/src/main/webapp/app/view/apps/appsdetails/appsdetails.component.spec.ts
@@ -21,10 +21,10 @@
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';
diff --git a/web/gui2/src/main/webapp/app/view/apps/appsdetails/appsdetails.component.ts b/web/gui2/src/main/webapp/app/view/apps/appsdetails/appsdetails.component.ts
index 1e6ed29..c0d2e02 100644
--- a/web/gui2/src/main/webapp/app/view/apps/appsdetails/appsdetails.component.ts
+++ b/web/gui2/src/main/webapp/app/view/apps/appsdetails/appsdetails.component.ts
@@ -101,8 +101,20 @@
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 {
diff --git a/web/gui2/src/main/webapp/app/view/device/device-routing.module.ts b/web/gui2/src/main/webapp/app/view/device/device-routing.module.ts
index 3110eca..64bcb92 100644
--- a/web/gui2/src/main/webapp/app/view/device/device-routing.module.ts
+++ b/web/gui2/src/main/webapp/app/view/device/device-routing.module.ts
@@ -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 = [
{
diff --git a/web/gui2/src/main/webapp/app/view/device/device.component.html b/web/gui2/src/main/webapp/app/view/device/device.component.html
deleted file mode 100644
index 0e09d69..0000000
--- a/web/gui2/src/main/webapp/app/view/device/device.component.html
+++ /dev/null
@@ -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>
diff --git a/web/gui2/src/main/webapp/app/view/device/device.component.spec.ts b/web/gui2/src/main/webapp/app/view/device/device.component.spec.ts
deleted file mode 100644
index 066a636..0000000
--- a/web/gui2/src/main/webapp/app/view/device/device.component.spec.ts
+++ /dev/null
@@ -1,141 +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 { 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 { DeviceComponent } from './device.component';
-
-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 { of } from 'rxjs';
-
-class MockActivatedRoute extends ActivatedRoute {
- constructor(params: Params) {
- super();
- this.queryParams = of(params);
- }
-}
-
-class MockDetailsPanelService {}
-
-class MockFnService {}
-
-class MockIconService {
- loadIconDef() {}
-}
-
-class MockGlyphService {}
-
-class MockKeyService {}
-
-class MockLoadingService {
- startAnim() {}
- stop() {}
-}
-
-class MockNavService {}
-
-class MockMastService {}
-
-class MockTableBuilderService {}
-
-class MockTableDetailService {}
-
-class MockThemeService {}
-
-class MockWebSocketService {
- createWebSocket() {}
- isConnected() { return false; }
- unbindHandlers() {}
- bindHandlers() {}
-}
-
-/**
- * ONOS GUI -- Device View Module - Unit Tests
- */
-describe('DeviceComponent', () => {
- let fs: FnService;
- let ar: MockActivatedRoute;
- let windowMock: Window;
- let logServiceSpy: jasmine.SpyObj<LogService>;
- let component: DeviceComponent;
- let fixture: ComponentFixture<DeviceComponent>;
-
- 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({
- declarations: [ DeviceComponent, IconComponent ],
- 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: LogService, useValue: logSpy },
- { provide: ThemeService, useClass: MockThemeService },
- { provide: WebSocketService, useClass: MockWebSocketService },
- { provide: 'Window', useValue: windowMock },
- ]
- })
- .compileComponents();
- logServiceSpy = TestBed.get(LogService);
- }));
-
- beforeEach(() => {
- fixture = TestBed.createComponent(DeviceComponent);
- component = fixture.debugElement.componentInstance;
- fixture.detectChanges();
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-
- it('should have .table-header with "Friendly Name..."', () => {
- const appDe: DebugElement = fixture.debugElement;
- const divDe = appDe.query(By.css('.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 ');
- });
-});
diff --git a/web/gui2/src/main/webapp/app/view/device/device.module.ts b/web/gui2/src/main/webapp/app/view/device/device.module.ts
index 7840292..9ad5456 100644
--- a/web/gui2/src/main/webapp/app/view/device/device.module.ts
+++ b/web/gui2/src/main/webapp/app/view/device/device.module.ts
@@ -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 { }
diff --git a/web/gui2/src/main/webapp/app/view/device/device.theme.css b/web/gui2/src/main/webapp/app/view/device/device.theme.css
deleted file mode 100644
index df0f139..0000000
--- a/web/gui2/src/main/webapp/app/view/device/device.theme.css
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * 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 -- Device View (theme) -- CSS file
- */
-
-.light #device-details-panel .bottom th {
- background-color: #e5e5e6;
-}
-
-.light #device-details-panel .bottom tr:nth-child(odd) {
- background-color: #fbfbfb;
-}
-.light #device-details-panel .bottom tr:nth-child(even) {
- background-color: #f4f4f4;
-}
-
diff --git a/web/gui2/src/main/webapp/app/view/device/device.component.css b/web/gui2/src/main/webapp/app/view/device/device/device.component.css
similarity index 97%
rename from web/gui2/src/main/webapp/app/view/device/device.component.css
rename to web/gui2/src/main/webapp/app/view/device/device/device.component.css
index 4d8454d..5ed16e8 100644
--- a/web/gui2/src/main/webapp/app/view/device/device.component.css
+++ b/web/gui2/src/main/webapp/app/view/device/device/device.component.css
@@ -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.
diff --git a/web/gui2/src/main/webapp/app/view/device/device/device.component.html b/web/gui2/src/main/webapp/app/view/device/device/device.component.html
new file mode 100644
index 0000000..7a22076
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/device/device/device.component.html
@@ -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>
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/device/device/device.component.spec.ts b/web/gui2/src/main/webapp/app/view/device/device/device.component.spec.ts
new file mode 100644
index 0000000..207dde9
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/device/device/device.component.spec.ts
@@ -0,0 +1,157 @@
+/*
+ * 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 { DebugElement } from '@angular/core';
+import { By } from '@angular/platform-browser';
+import { LogService } from '../../../log.service';
+import { DeviceComponent } from './device.component';
+import { } from 'jasmine';
+
+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) {
+ 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 -- Device View Module - Unit Tests
+ */
+describe('DeviceComponent', () => {
+ let fs: FnService;
+ let ar: MockActivatedRoute;
+ let windowMock: Window;
+ let logServiceSpy: jasmine.SpyObj<LogService>;
+ let component: DeviceComponent;
+ let fixture: ComponentFixture<DeviceComponent>;
+
+ 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: [DeviceComponent, IconComponent, TableFilterPipe, DeviceDetailsComponent],
+ 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: LogService, useValue: logSpy },
+ { provide: ThemeService, useClass: MockThemeService },
+ { provide: WebSocketService, useClass: MockWebSocketService },
+ { provide: 'Window', useValue: windowMock },
+ ]
+ }).compileComponents();
+ logServiceSpy = TestBed.get(LogService);
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(DeviceComponent);
+ component = fixture.debugElement.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ 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 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();
+ });
+});
diff --git a/web/gui2/src/main/webapp/app/view/device/device.component.ts b/web/gui2/src/main/webapp/app/view/device/device/device.component.ts
similarity index 61%
rename from web/gui2/src/main/webapp/app/view/device/device.component.ts
rename to web/gui2/src/main/webapp/app/view/device/device/device.component.ts
index ecccc34..b6f1d95 100644
--- a/web/gui2/src/main/webapp/app/view/device/device.component.ts
+++ b/web/gui2/src/main/webapp/app/view/device/device/device.component.ts
@@ -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 @@
* 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 @@
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 @@
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 } });
+ }
+ }
+
}
diff --git a/web/gui2/src/main/webapp/app/view/device/device/device.theme.css b/web/gui2/src/main/webapp/app/view/device/device/device.theme.css
new file mode 100644
index 0000000..d9b2c07
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/device/device/device.theme.css
@@ -0,0 +1,60 @@
+/*
+ * 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 -- Device View (theme) -- CSS file
+ */
+
+.light #device-details-panel .bottom th {
+ background-color: #e5e5e6;
+}
+
+.light #device-details-panel .bottom tr:nth-child(odd) {
+ background-color: #fbfbfb;
+}
+.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;
+}
diff --git a/web/gui2/src/main/webapp/app/view/device/device.component.css b/web/gui2/src/main/webapp/app/view/device/devicedetails/devicedetails.component.css
similarity index 75%
copy from web/gui2/src/main/webapp/app/view/device/device.component.css
copy to web/gui2/src/main/webapp/app/view/device/devicedetails/devicedetails.component.css
index 4d8454d..a3903b0 100644
--- a/web/gui2/src/main/webapp/app/view/device/device.component.css
+++ b/web/gui2/src/main/webapp/app/view/device/devicedetails/devicedetails.component.css
@@ -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.
@@ -14,36 +14,24 @@
* limitations under the License.
*/
-/*
- ONOS GUI -- Device View (layout) -- CSS file
- */
-#ov-device .tabular-header {
- text-align: left;
-}
-
-#ov-device h2 {
- display: inline-block;
-}
-
-#ov-device, div.ctrl-btns {
-}
-
-
-/* More in generic panel.css */
-
#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: 8px 12px;
+ padding: 0 30px;
}
#device-details-panel .close-btn {
position: absolute;
- right: 12px;
- top: 12px;
+ right:5px;
+ top: 5px;
cursor: pointer;
}
@@ -56,33 +44,15 @@
#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 .top-tables {
- font-size: 10pt;
- white-space: nowrap;
-}
-
-#device-details-panel .top div.left {
- float: left;
- padding: 0 18px 0 0;
-}
-#device-details-panel .top div.right {
- display: inline-block;
-}
-
-#device-details-panel td.label {
- font-weight: bold;
- text-align: right;
- padding-right: 6px;
-}
-
#device-details-panel .actionBtns div {
padding: 12px 6px;
}
@@ -92,8 +62,23 @@
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 {
@@ -106,3 +91,24 @@
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;
+}
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/device/devicedetails/devicedetails.component.html b/web/gui2/src/main/webapp/app/view/device/devicedetails/devicedetails.component.html
new file mode 100644
index 0000000..034b63b
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/device/devicedetails/devicedetails.component.html
@@ -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>
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/device/devicedetails/devicedetails.component.spec.ts b/web/gui2/src/main/webapp/app/view/device/devicedetails/devicedetails.component.spec.ts
new file mode 100644
index 0000000..5daa2ef
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/device/devicedetails/devicedetails.component.spec.ts
@@ -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();
+ });
+});
diff --git a/web/gui2/src/main/webapp/app/view/device/devicedetails/devicedetails.component.ts b/web/gui2/src/main/webapp/app/view/device/devicedetails/devicedetails.component.ts
new file mode 100644
index 0000000..9e30eb7
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/device/devicedetails/devicedetails.component.ts
@@ -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);
+ }
+ }
+
+}
diff --git a/web/gui2/src/main/webapp/app/view/device/devicedetailspanel.directive.ts b/web/gui2/src/main/webapp/app/view/device/devicedetailspanel.directive.ts
deleted file mode 100644
index d81a67d..0000000
--- a/web/gui2/src/main/webapp/app/view/device/devicedetailspanel.directive.ts
+++ /dev/null
@@ -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');
- }
-
-}
diff --git a/web/gui2/src/main/webapp/app/view/flow/flow-routing.module.ts b/web/gui2/src/main/webapp/app/view/flow/flow-routing.module.ts
new file mode 100644
index 0000000..4ddf65f
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/flow/flow-routing.module.ts
@@ -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 { }
diff --git a/web/gui2/src/main/webapp/app/view/flow/flow.module.ts b/web/gui2/src/main/webapp/app/view/flow/flow.module.ts
new file mode 100644
index 0000000..55d3c96
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/flow/flow.module.ts
@@ -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 { }
diff --git a/web/gui2/src/main/webapp/app/view/flow/flow/flow.component.css b/web/gui2/src/main/webapp/app/view/flow/flow/flow.component.css
new file mode 100644
index 0000000..3ed4d16
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/flow/flow/flow.component.css
@@ -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;
+}
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/flow/flow/flow.component.html b/web/gui2/src/main/webapp/app/view/flow/flow/flow.component.html
new file mode 100644
index 0000000..a1b23df
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/flow/flow/flow.component.html
@@ -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>
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/flow/flow/flow.component.spec.ts b/web/gui2/src/main/webapp/app/view/flow/flow/flow.component.spec.ts
new file mode 100644
index 0000000..e6ed12a
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/flow/flow/flow.component.spec.ts
@@ -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();
+ });
+});
diff --git a/web/gui2/src/main/webapp/app/view/flow/flow/flow.component.ts b/web/gui2/src/main/webapp/app/view/flow/flow/flow.component.ts
new file mode 100644
index 0000000..96692a4
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/flow/flow/flow.component.ts
@@ -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;
+ }
+
+}
diff --git a/web/gui2/src/main/webapp/app/view/flow/flow/flow.theme.css b/web/gui2/src/main/webapp/app/view/flow/flow/flow.theme.css
new file mode 100644
index 0000000..37738a9
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/flow/flow/flow.theme.css
@@ -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;
+}
diff --git a/web/gui2/src/main/webapp/app/view/flow/flowdetails/flowdetails/flowdetails.component.css b/web/gui2/src/main/webapp/app/view/flow/flowdetails/flowdetails/flowdetails.component.css
new file mode 100644
index 0000000..2f11349
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/flow/flowdetails/flowdetails/flowdetails.component.css
@@ -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;
+}
diff --git a/web/gui2/src/main/webapp/app/view/flow/flowdetails/flowdetails/flowdetails.component.html b/web/gui2/src/main/webapp/app/view/flow/flowdetails/flowdetails/flowdetails.component.html
new file mode 100644
index 0000000..05d0a47
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/flow/flowdetails/flowdetails/flowdetails.component.html
@@ -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>
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/flow/flowdetails/flowdetails/flowdetails.component.spec.ts b/web/gui2/src/main/webapp/app/view/flow/flowdetails/flowdetails/flowdetails.component.spec.ts
new file mode 100644
index 0000000..8bbf482
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/flow/flowdetails/flowdetails/flowdetails.component.spec.ts
@@ -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();
+ });
+});
diff --git a/web/gui2/src/main/webapp/app/view/flow/flowdetails/flowdetails/flowdetails.component.ts b/web/gui2/src/main/webapp/app/view/flow/flowdetails/flowdetails/flowdetails.component.ts
new file mode 100644
index 0000000..3197285
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/flow/flowdetails/flowdetails/flowdetails.component.ts
@@ -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);
+ }
+}
diff --git a/web/gui2/src/main/webapp/app/view/group/group-routing.module.ts b/web/gui2/src/main/webapp/app/view/group/group-routing.module.ts
new file mode 100644
index 0000000..9ce3153
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/group/group-routing.module.ts
@@ -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 { }
diff --git a/web/gui2/src/main/webapp/app/view/group/group.module.ts b/web/gui2/src/main/webapp/app/view/group/group.module.ts
new file mode 100644
index 0000000..343a971
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/group/group.module.ts
@@ -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 { }
diff --git a/web/gui2/src/main/webapp/app/view/group/group/group.component.css b/web/gui2/src/main/webapp/app/view/group/group/group.component.css
new file mode 100644
index 0000000..fbf19de
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/group/group/group.component.css
@@ -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;
+}
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/group/group/group.component.html b/web/gui2/src/main/webapp/app/view/group/group/group.component.html
new file mode 100644
index 0000000..4b0afbb
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/group/group/group.component.html
@@ -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>
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/group/group/group.component.spec.ts b/web/gui2/src/main/webapp/app/view/group/group/group.component.spec.ts
new file mode 100644
index 0000000..bcf0ca2
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/group/group/group.component.spec.ts
@@ -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();
+ });
+});
diff --git a/web/gui2/src/main/webapp/app/view/group/group/group.component.ts b/web/gui2/src/main/webapp/app/view/group/group/group.component.ts
new file mode 100644
index 0000000..fc6e9bd
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/group/group/group.component.ts
@@ -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;
+ }
+
+}
diff --git a/web/gui2/src/main/webapp/app/view/group/group/group.theme.css b/web/gui2/src/main/webapp/app/view/group/group/group.theme.css
new file mode 100644
index 0000000..f90556c
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/group/group/group.theme.css
@@ -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;
+}
diff --git a/web/gui2/src/main/webapp/app/view/host/host-routing.module.ts b/web/gui2/src/main/webapp/app/view/host/host-routing.module.ts
new file mode 100644
index 0000000..62f10a1
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/host/host-routing.module.ts
@@ -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 { }
diff --git a/web/gui2/src/main/webapp/app/view/host/host.module.ts b/web/gui2/src/main/webapp/app/view/host/host.module.ts
new file mode 100644
index 0000000..230b55a
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/host/host.module.ts
@@ -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 { }
diff --git a/web/gui2/src/main/webapp/app/view/host/host/host.component.css b/web/gui2/src/main/webapp/app/view/host/host/host.component.css
new file mode 100644
index 0000000..d306a23
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/host/host/host.component.css
@@ -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;
+}
diff --git a/web/gui2/src/main/webapp/app/view/host/host/host.component.html b/web/gui2/src/main/webapp/app/view/host/host/host.component.html
new file mode 100644
index 0000000..7c7e32c
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/host/host/host.component.html
@@ -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>
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/host/host/host.component.spec.ts b/web/gui2/src/main/webapp/app/view/host/host/host.component.spec.ts
new file mode 100644
index 0000000..6d915c7
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/host/host/host.component.spec.ts
@@ -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();
+ });
+
+});
diff --git a/web/gui2/src/main/webapp/app/view/host/host/host.component.ts b/web/gui2/src/main/webapp/app/view/host/host/host.component.ts
new file mode 100644
index 0000000..c13e2f5
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/host/host/host.component.ts
@@ -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');
+ }
+
+}
diff --git a/web/gui2/src/main/webapp/app/view/host/hostdetails/hostdetails.component.css b/web/gui2/src/main/webapp/app/view/host/hostdetails/hostdetails.component.css
new file mode 100644
index 0000000..39b0131
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/host/hostdetails/hostdetails.component.css
@@ -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;
+}
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/host/hostdetails/hostdetails.component.html b/web/gui2/src/main/webapp/app/view/host/hostdetails/hostdetails.component.html
new file mode 100644
index 0000000..c7c9dba
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/host/hostdetails/hostdetails.component.html
@@ -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>
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/host/hostdetails/hostdetails.component.spec.ts b/web/gui2/src/main/webapp/app/view/host/hostdetails/hostdetails.component.spec.ts
new file mode 100644
index 0000000..0ce1ba0
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/host/hostdetails/hostdetails.component.spec.ts
@@ -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();
+ });
+});
diff --git a/web/gui2/src/main/webapp/app/view/host/hostdetails/hostdetails.component.ts b/web/gui2/src/main/webapp/app/view/host/hostdetails/hostdetails.component.ts
new file mode 100644
index 0000000..7d1132d
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/host/hostdetails/hostdetails.component.ts
@@ -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);
+ }
+ }
+
+}
diff --git a/web/gui2/src/main/webapp/app/view/link/link-routing.module.ts b/web/gui2/src/main/webapp/app/view/link/link-routing.module.ts
new file mode 100644
index 0000000..a2ff939
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/link/link-routing.module.ts
@@ -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 { }
diff --git a/web/gui2/src/main/webapp/app/view/link/link.module.ts b/web/gui2/src/main/webapp/app/view/link/link.module.ts
new file mode 100644
index 0000000..92276b0
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/link/link.module.ts
@@ -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 { }
diff --git a/web/gui2/src/main/webapp/app/view/link/link/link.component.css b/web/gui2/src/main/webapp/app/view/link/link/link.component.css
new file mode 100644
index 0000000..a7c04e2
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/link/link/link.component.css
@@ -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;
+}
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/link/link/link.component.html b/web/gui2/src/main/webapp/app/view/link/link/link.component.html
new file mode 100644
index 0000000..e90ede8
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/link/link/link.component.html
@@ -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>
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/link/link/link.component.spec.ts b/web/gui2/src/main/webapp/app/view/link/link/link.component.spec.ts
new file mode 100644
index 0000000..699e535
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/link/link/link.component.spec.ts
@@ -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();
+ });
+});
diff --git a/web/gui2/src/main/webapp/app/view/link/link/link.component.ts b/web/gui2/src/main/webapp/app/view/link/link/link.component.ts
new file mode 100644
index 0000000..eeb7c33
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/link/link/link.component.ts
@@ -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');
+ }
+}
diff --git a/web/gui2/src/main/webapp/app/view/meter/meter-routing.module.ts b/web/gui2/src/main/webapp/app/view/meter/meter-routing.module.ts
new file mode 100644
index 0000000..83a432e
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/meter/meter-routing.module.ts
@@ -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 { }
diff --git a/web/gui2/src/main/webapp/app/view/meter/meter.module.ts b/web/gui2/src/main/webapp/app/view/meter/meter.module.ts
new file mode 100644
index 0000000..90263b2
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/meter/meter.module.ts
@@ -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 { }
diff --git a/web/gui2/src/main/webapp/app/view/meter/meter/meter.component.css b/web/gui2/src/main/webapp/app/view/meter/meter/meter.component.css
new file mode 100644
index 0000000..809ab49
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/meter/meter/meter.component.css
@@ -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;
+}
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/meter/meter/meter.component.html b/web/gui2/src/main/webapp/app/view/meter/meter/meter.component.html
new file mode 100644
index 0000000..bfeb407
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/meter/meter/meter.component.html
@@ -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>
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/meter/meter/meter.component.spec.ts b/web/gui2/src/main/webapp/app/view/meter/meter/meter.component.spec.ts
new file mode 100644
index 0000000..e17ff50
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/meter/meter/meter.component.spec.ts
@@ -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();
+ });
+});
diff --git a/web/gui2/src/main/webapp/app/view/meter/meter/meter.component.ts b/web/gui2/src/main/webapp/app/view/meter/meter/meter.component.ts
new file mode 100644
index 0000000..e026e1c
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/meter/meter/meter.component.ts
@@ -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;
+ }
+
+}
diff --git a/web/gui2/src/main/webapp/app/view/meter/meter/meter.theme.css b/web/gui2/src/main/webapp/app/view/meter/meter/meter.theme.css
new file mode 100644
index 0000000..9f4dea4
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/meter/meter/meter.theme.css
@@ -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;
+}
+
+
diff --git a/web/gui2/src/main/webapp/app/view/port/port-routing.module.ts b/web/gui2/src/main/webapp/app/view/port/port-routing.module.ts
new file mode 100644
index 0000000..6ba681d
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/port/port-routing.module.ts
@@ -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 { }
diff --git a/web/gui2/src/main/webapp/app/view/port/port.module.ts b/web/gui2/src/main/webapp/app/view/port/port.module.ts
new file mode 100644
index 0000000..dd622dc
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/port/port.module.ts
@@ -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 { }
diff --git a/web/gui2/src/main/webapp/app/view/port/port/port.component.css b/web/gui2/src/main/webapp/app/view/port/port/port.component.css
new file mode 100644
index 0000000..015d475
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/port/port/port.component.css
@@ -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;
+}
diff --git a/web/gui2/src/main/webapp/app/view/port/port/port.component.html b/web/gui2/src/main/webapp/app/view/port/port/port.component.html
new file mode 100644
index 0000000..8625d1a
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/port/port/port.component.html
@@ -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>
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/port/port/port.component.spec.ts b/web/gui2/src/main/webapp/app/view/port/port/port.component.spec.ts
new file mode 100644
index 0000000..233b34e
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/port/port/port.component.spec.ts
@@ -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();
+ });
+});
diff --git a/web/gui2/src/main/webapp/app/view/port/port/port.component.ts b/web/gui2/src/main/webapp/app/view/port/port/port.component.ts
new file mode 100644
index 0000000..1c86c9c
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/port/port/port.component.ts
@@ -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']);
+ }
+
+}
diff --git a/web/gui2/src/main/webapp/app/view/port/portdetails/portdetails.component.css b/web/gui2/src/main/webapp/app/view/port/portdetails/portdetails.component.css
new file mode 100644
index 0000000..225679d
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/port/portdetails/portdetails.component.css
@@ -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;
+}
diff --git a/web/gui2/src/main/webapp/app/view/port/portdetails/portdetails.component.html b/web/gui2/src/main/webapp/app/view/port/portdetails/portdetails.component.html
new file mode 100644
index 0000000..36fa847
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/port/portdetails/portdetails.component.html
@@ -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>
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/port/portdetails/portdetails.component.spec.ts b/web/gui2/src/main/webapp/app/view/port/portdetails/portdetails.component.spec.ts
new file mode 100644
index 0000000..18dc37b
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/port/portdetails/portdetails.component.spec.ts
@@ -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 :');
+ });
+});
diff --git a/web/gui2/src/main/webapp/app/view/port/portdetails/portdetails.component.ts b/web/gui2/src/main/webapp/app/view/port/portdetails/portdetails.component.ts
new file mode 100644
index 0000000..1fd62d3
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/port/portdetails/portdetails.component.ts
@@ -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);
+ }
+ }
+
+}
diff --git a/web/gui2/src/main/webapp/app/view/tunnel/tunnel-routing.module.ts b/web/gui2/src/main/webapp/app/view/tunnel/tunnel-routing.module.ts
new file mode 100644
index 0000000..dbbee7f
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/tunnel/tunnel-routing.module.ts
@@ -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 { }
diff --git a/web/gui2/src/main/webapp/app/view/tunnel/tunnel.module.spec.ts b/web/gui2/src/main/webapp/app/view/tunnel/tunnel.module.spec.ts
new file mode 100644
index 0000000..694f645
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/tunnel/tunnel.module.spec.ts
@@ -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();
+ });
+});
diff --git a/web/gui2/src/main/webapp/app/view/tunnel/tunnel.module.ts b/web/gui2/src/main/webapp/app/view/tunnel/tunnel.module.ts
new file mode 100644
index 0000000..56e03b6
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/tunnel/tunnel.module.ts
@@ -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 { }
diff --git a/web/gui2/src/main/webapp/app/view/tunnel/tunnel/tunnel.component.css b/web/gui2/src/main/webapp/app/view/tunnel/tunnel/tunnel.component.css
new file mode 100644
index 0000000..73472ff
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/tunnel/tunnel/tunnel.component.css
@@ -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;
+}
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/tunnel/tunnel/tunnel.component.html b/web/gui2/src/main/webapp/app/view/tunnel/tunnel/tunnel.component.html
new file mode 100644
index 0000000..5829acf
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/tunnel/tunnel/tunnel.component.html
@@ -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>
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/view/tunnel/tunnel/tunnel.component.spec.ts b/web/gui2/src/main/webapp/app/view/tunnel/tunnel/tunnel.component.spec.ts
new file mode 100644
index 0000000..3b2611e
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/tunnel/tunnel/tunnel.component.spec.ts
@@ -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();
+ });
+});
diff --git a/web/gui2/src/main/webapp/app/view/tunnel/tunnel/tunnel.component.ts b/web/gui2/src/main/webapp/app/view/tunnel/tunnel/tunnel.component.ts
new file mode 100644
index 0000000..bbd4b67
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/tunnel/tunnel/tunnel.component.ts
@@ -0,0 +1,84 @@
+/*
+ * 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 { TableResponse, TableBaseImpl, SortDir } 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 { WebSocketService } from '../../../fw/remote/websocket.service';
+
+/**
+ * Model of the response from WebSocket
+ */
+interface TunnelTableResponse extends TableResponse {
+ tunnels: Tunnel[];
+}
+
+/**
+ * Model of the tunnels returned from the WebSocket
+ */
+interface Tunnel {
+ id: string;
+ name: string;
+ port1: string;
+ port2: string;
+ type: string;
+ groupId: string;
+ bandwidth: string;
+ path: string;
+}
+
+/**
+ * ONOS GUI -- Tunnel View Component
+ */
+@Component({
+ selector: 'onos-tunnel',
+ templateUrl: './tunnel.component.html',
+ styleUrls: ['./tunnel.component.css', '../../../fw/widget/table.css', '../../../fw/widget/table.theme.css']
+})
+export class TunnelComponent extends TableBaseImpl implements OnInit, OnDestroy {
+
+ constructor(
+ protected fs: FnService,
+ protected ls: LoadingService,
+ protected log: LogService,
+ protected wss: WebSocketService,
+ ) {
+ super(fs, ls, log, wss, 'tunnel');
+ this.responseCallback = this.tunnelResponseCb;
+ this.sortParams = {
+ firstCol: 'id',
+ firstDir: SortDir.desc,
+ secondCol: 'name',
+ secondDir: SortDir.asc,
+ };
+ }
+
+ ngOnInit() {
+ this.init();
+ this.log.debug('TunnelComponent initialized');
+ }
+
+ ngOnDestroy() {
+ this.destroy();
+ this.log.debug('TunnelComponent destroyed');
+ }
+
+ tunnelResponseCb(data: TunnelTableResponse) {
+ this.log.debug('Tunnel response received for ', data.tunnels.length, 'tunnels');
+ }
+
+}