diff --git a/web/gui2/src/main/webapp/app/README.txt b/web/gui2/src/main/webapp/app/README.txt index cffc8a147c..820f36ed73 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 caa52ecbf4..79e4153d62 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 { ActivatedRoute, Params } from '@angular/router'; import { VeilComponent } from './veil.component'; import { ConsoleLoggerService } from '../../../consolelogger.service'; -import { FnService } from '../../../fw/util/fn.service'; +import { FnService } from '../../util/fn.service'; import { LogService } from '../../../log.service'; import { KeyService } from '../../util/key.service'; import { GlyphService } from '../../svg/glyph.service'; 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 cb48492126..f807c9a46a 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 @@ \ 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 e2bd9999bf..7079b15436 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 { By } from '@angular/platform-browser'; import { of } from 'rxjs'; import { ConsoleLoggerService } from '../../../consolelogger.service'; -import { FnService } from '../../../fw/util/fn.service'; +import { FnService } from '../../util/fn.service'; import { IconComponent } from '../../svg/icon/icon.component'; import { IconService } from '../../svg/icon.service'; import { LionService } from '../../util/lion.service'; @@ -127,6 +127,6 @@ describe('NavComponent', () => { const appDe: DebugElement = fixture.debugElement; const divDe = appDe.query(By.css('nav#nav a')); const div: HTMLElement = divDe.nativeElement; - expect(div.textContent).toEqual('Apps'); + expect(div.textContent).toEqual(' Apps'); }); }); 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 7e8adb46a9..0c1eff3dc2 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 @@ const rgs = 'registerGlyphSet(): '; export class GlyphService { // internal state glyphs = d3.map(); + api: Object; constructor( private fs: FnService, -// private gd: GlyphDataService, - private log: LogService + // private gd: GlyphDataService, + private log: LogService, + private sus: SvgUtilService ) { this.clear(); this.init(); + this.api = { + registerGlyphs: this.registerGlyphs, + registerGlyphSet: this.registerGlyphSet, + ids: this.ids, + glyph: this.glyph, + glyphDefined: this.glyphDefined, + loadDefs: this.loadDefs, + addGlyph: this.addGlyph, + }; this.log.debug('GlyphService constructed'); } @@ -121,7 +133,7 @@ export class GlyphService { } for (const [key, value] of data.entries()) { -// angular.forEach(data, function (value, key) { + // angular.forEach(data, function (value, key) { if (key[0] !== '_') { this.addToMap(key, value, vb, overwrite, dups); } @@ -167,11 +179,28 @@ export class GlyphService { } } defs.append('symbol') - .attr('id', g.id) - .attr('viewBox', g.vb) - .append('path') - .attr('d', g.d); + .attr('id', g.id) + .attr('viewBox', g.vb) + .append('path') + .attr('d', g.d); } }); } + + addGlyph(elem: any, glyphId: string, size: number, overlay: any, trans: any) { + const sz = size || 40, + ovr = !!overlay, + xns = this.fs.isA(trans), + atr = { + width: sz, + height: sz, + 'class': 'glyph', + 'xlink:href': '#' + glyphId, + }; + + if (xns) { + atr.class = this.sus.translate(trans); + } + return elem.append('use').attr(atr).classed('overlay', ovr); + } } 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 813589f1fb..de3672ce28 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 @@ export const glyphMapping = new Map([ ['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 347837a9df..d2d6f56628 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 @@ svg.embeddedIcon g.icon { fill: none; } -svg.embeddedIcon g.icon rect { - stroke: none; - fill: none; +.ctrl-btns div svg.embeddedIcon g.icon use { + fill: #e0dfd6; } -svg.embeddedIcon g.icon .glyph { - stroke: none; - fill-rule: evenodd; +.ctrl-btns div.active svg.embeddedIcon g.icon use { + fill: #939598; +} +.ctrl-btns div.active:hover svg.embeddedIcon g.icon use { + fill: #ce5b58; } -svg.embeddedIcon .icon.appInactive .glyph { - fill: none !important; +.ctrl-btns div.current-view svg.embeddedIcon g.icon rect { + fill: #518ecc; } +.ctrl-btns div.current-view svg.embeddedIcon g.icon use { + fill: white; +} + +svg.embeddedIcon .icon.active .glyph { + fill: #04bf34; +} + +svg.embeddedIcon .icon.inactive .glyph { + fill: #c0242b; +} + +svg.embeddedIcon .icon.active-rect .glyph { + fill:#939598; +} + +svg.embeddedIcon .icon.active-sort .glyph { + fill:#333333; +} + +svg.embeddedIcon g.icon.active-rect:hover use { + fill: #ce5b58; +} + +svg.embeddedIcon g.icon.active-type .glyph { + fill: #3c3a3a; +} + +svg.embeddedIcon g.icon.active-close:hover use { + fill: #ce5b58; +} + +svg.embeddedIcon g.icon.active-close .glyph { + fill: #333333; +} + +svg.embeddedIcon g.icon.details-icon .glyph { + fill: #0071bd;; +} + + 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 5be4c1976c..2243dc2bf9 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. --> +
@@ -20,4 +21,8 @@ -

{{ toolTipDisp }}

+ + + + {{toolTipDisp}} +
\ 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 3e6f601a1d..ca6e32eb6d 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 @@ svg.embeddedIcon g.icon use { /* ACTIVE */ -svg.embeddedIcon g.icon.active use { +.ctrl-btns div.active svg.embeddedIcon g.icon use { fill: #939598; } + svg.embeddedIcon g.icon.active:hover use { fill: #ce5b58; } @@ -82,25 +70,25 @@ svg.embeddedIcon g.icon.refresh.active:hover use { /* ========== DARK Theme ========== */ -.dark div.close-btn svg.embeddedIcon g.icon .glyph { + div.close-btn svg.embeddedIcon g.icon .glyph { fill: #8d8d8d; } -.dark div.tableColSort svg.embeddedIcon .icon .glyph { + div.tableColSort svg.embeddedIcon .icon .glyph { fill: #888888; } -.dark svg.embeddedIcon .icon.active .glyph { + /*svg.embeddedIcon .icon.active .glyph { fill: #04bf34; } - -.dark svg.embeddedIcon .icon.inactive .glyph { + svg.embeddedIcon .icon.inactive .glyph { fill: #c0242b; -} +}*/ -.dark table svg.embeddedIcon .icon .glyph { + table svg.embeddedIcon .icon .glyph { fill: #9999aa; } + /* svg.embeddedIcon g.icon .glyph { fill: #007dc4; @@ -109,4 +97,12 @@ svg.embeddedIcon g.icon .glyph { svg.embeddedIcon:hover g.icon .glyph { fill: #20b2ff; } -*/ \ 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 74a5443d34..890f1f89d2 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 9cba079e17..23759ab42f 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 @@ import { LogService } from '../../log.service'; * 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 1eaa895c49..4c6f492cf7 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 f696125716..991df7fea7 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 @@ import { WebSocketService } from '../remote/websocket.service'; * ONOS GUI -- Util -- User Preference Service */ @Injectable({ - providedIn: 'root', + providedIn: 'root', }) export class PrefsService { - + protected Prefs; + protected handlers: string[] = []; + cache: any; + listeners: any; constructor( - private fs: FnService, - private log: LogService, - private wss: WebSocketService + protected fs: FnService, + protected log: LogService, + protected wss: WebSocketService ) { + this.cache = {}; + this.wss.bindHandlers(new Map 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 d808e489b2..1b6e46639a 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 @@ import { LogService } from '../../log.service'; 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 d7cf7b8bc5..59f85c5c90 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 { LogService } from '../../log.service'; import { WebSocketService } from '../remote/websocket.service'; import { PanelBaseImpl } from './panel.base'; +import { Output, EventEmitter, Input } from '@angular/core'; /** * A generic model of the data returned from the *DetailsResponse @@ -37,6 +38,9 @@ interface DetailsResponse { */ export abstract class DetailsPanelBaseImpl extends PanelBaseImpl { + @Input() id: string; + @Output() closeEvent = new EventEmitter(); + private root: string; private req: string; private resp: string; @@ -85,16 +89,10 @@ export abstract class DetailsPanelBaseImpl extends PanelBaseImpl { } /** - * Details Panel Data Request - should be called whenever id changes - * If id is empty, no request is made + * Details Panel Data Request - should be called whenever row id changes */ - requestDetailsPanelData(id: string) { - if (id === '') { - return; - } + requestDetailsPanelData(query: any) { this.closed = false; - const query = {'id': id}; - // Do not send if the Web Socket hasn't opened if (this.wss.isConnected()) { if (this.fs.debugOn('panel')) { @@ -109,5 +107,8 @@ export abstract class DetailsPanelBaseImpl extends PanelBaseImpl { */ close(): void { this.closed = true; + this.id = null; + this.closeEvent.emit(this.id); } + } 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 34d127f038..48530acd16 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 0093f72311..cc29878aef 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 @@ export interface SortParams { secondDir: SortDir; } +export interface PayloadParams { + devId: string; +} + + /** * ONOS GUI -- Widget -- Table Base class */ @@ -74,7 +79,7 @@ export abstract class TableBaseImpl { // attributes from the interface protected annots: TableAnnots; protected changedData: string[] = []; - protected payloadParams: any; + protected payloadParams: PayloadParams; protected sortParams: SortParams; protected selectCallback; // Function protected parentSelCb = null; @@ -164,7 +169,7 @@ export abstract class TableBaseImpl { if (JSON.stringify(newTableData) !== JSON.stringify(this.tableData)) { this.log.debug('table data has changed'); const oldTableData: any[] = this.tableData; - this.tableData = [ ...newTableData ]; // ES6 spread syntax + this.tableData = [...newTableData]; // ES6 spread syntax // only flash the row if the data already exists if (oldTableData.length > 0) { for (const idx in newTableData) { @@ -282,4 +287,12 @@ export abstract class TableBaseImpl { return ''; } } + + /** + * De-selects the row + */ + deselectRow(event) { + this.log.debug('Details panel close event'); + this.selId = event; + } } 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 9df99efe6a..1ed43b4db7 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 table { div.summary-list div.table-body { overflow-y: scroll; - max-height:70vh; } div.summary-list div.table-body::-webkit-scrollbar { display: none; } -div.summary-list tr.no-data td { +div.summary-list div.table-body tr.no-data td { text-align: center; font-style: italic; } 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 fef5123ee5..21ff59c66d 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 @@ import { LogService } from '../../log.service'; @Directive({ selector: '[onosTableResize]', }) -export class TableResizeDirective { +export class TableResizeDirective implements AfterContentChecked { - constructor( - private fs: FnService, - public log: LogService, - private el: ElementRef, - ) { + pdg = 22; + tables: any; - this.windowSize(); - this.log.debug('TableResizeDirective constructed'); + constructor(protected fs: FnService, + protected log: LogService, + protected mast: MastService, + protected el: ElementRef, + @Inject('Window') private w: Window) { + + log.info('TableResizeDirective constructed'); } - windowSize() { + ngAfterContentChecked() { + this.tables = { + thead: d3.select('div.table-header').select('table'), + tbody: d3.select('div.table-body').select('table') + }; + this.windowSize(this.tables); + } + + windowSize(tables: any) { const wsz = this.fs.windowSize(0, 30); - this.el.nativeElement.style.width = wsz.width + 'px'; + this.adjustTable(tables, wsz.width, wsz.height); } + + @HostListener('window:resize', ['event']) + onResize(event: any) { + this.windowSize(this.tables); + return { + h: this.w.innerHeight, + w: this.w.innerWidth + }; + } + + adjustTable(tables: any, width: number, height: number) { + this._width(tables.thead, width + 'px'); + this._width(tables.tbody, width + 'px'); + + this.setHeight(tables.thead, d3.select('div.table-body'), height); + } + + _width(elem, width) { + elem.style('width', width); + } + + setHeight(thead: any, body: any, height: number) { + const h = height - (this.mast.mastHeight + + this.fs.noPxStyle(d3.select('.tabular-header'), 'height') + + this.fs.noPxStyle(thead, 'height') + this.pdg); + body.style('height', h + 'px'); + } + } 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 5d4af2bd00..040139b915 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 @@ import { Routes, RouterModule } from '@angular/router'; */ const onosRoutes: Routes = [ { - path: 'apps', + path: 'app', loadChildren: 'app/view/apps/apps.module#AppsModule' }, { - path: 'devices', + path: 'device', loadChildren: 'app/view/device/device.module#DeviceModule' }, + { + path: 'link', + loadChildren: 'app/view/link/link.module#LinkModule' + }, + { + path: 'host', + loadChildren: 'app/view/host/host.module#HostModule' + }, + { + path: 'tunnel', + loadChildren: 'app/view/tunnel/tunnel.module#TunnelModule' + }, + { + path: 'flow', + loadChildren: 'app/view/flow/flow.module#FlowModule' + }, + { + path: 'port', + loadChildren: 'app/view/port/port.module#PortModule' + }, + { + path: 'group', + loadChildren: 'app/view/group/group.module#GroupModule' + }, + { + path: 'meter', + loadChildren: 'app/view/meter/meter.module#MeterModule' + }, { path: '', - redirectTo: 'devices', // Default to devices view - change to topo in future + redirectTo: 'device', // Default to devices view - change to topo in future pathMatch: 'full' } ]; 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 738ee18e3f..0e79a2b363 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 3096bae100..636fb0c7cf 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 943e26bf33..37826fa1f5 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 @@
-
-
- +
-
- +
-
- +
-
- +
@@ -74,34 +74,34 @@ -
+
- +
- + {{lionFn('title')}} - + {{lionFn('app_id')}} - + {{lionFn('version')}} - + {{lionFn('category')}} - + {{lionFn('origin')}} - +
- +
{{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. --> - + 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 cddf9aee2d..e022ebccba 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 { TableFilterPipe } from '../../../fw/widget/tablefilter.pipe'; import { UrlFnService } from '../../../fw/remote/urlfn.service'; import { WebSocketService } from '../../../fw/remote/websocket.service'; import { of } from 'rxjs'; +import { } from 'jasmine'; class MockActivatedRoute extends ActivatedRoute { constructor(params: Params) { @@ -46,33 +47,33 @@ class MockActivatedRoute extends ActivatedRoute { } } -class MockDialogService {} +class MockDialogService { } -class MockFnService {} +class MockFnService { } class MockHttpClient {} class MockIconService { - loadIconDef() {} + loadIconDef() { } } -class MockKeyService {} +class MockKeyService { } class MockLoadingService { - startAnim() {} - stop() {} - waiting() {} + startAnim() { } + stop() { } + waiting() { } } -class MockThemeService {} +class MockThemeService { } -class MockUrlFnService {} +class MockUrlFnService { } class MockWebSocketService { - createWebSocket() {} + createWebSocket() { } isConnected() { return false; } - unbindHandlers() {} - bindHandlers() {} + unbindHandlers() { } + bindHandlers() { } } /** @@ -90,21 +91,21 @@ describe('AppsComponent', () => { test: 'test1' } }; - const mockLion = (key) => { + const mockLion = (key) => { return bundleObj[key] || '%' + key + '%'; }; beforeEach(async(() => { const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']); - ar = new MockActivatedRoute({'debug': 'txrx'}); + ar = new MockActivatedRoute({ 'debug': 'txrx' }); windowMock = { - location: { + location: { hostname: 'foo', host: 'foo', port: '80', protocol: 'http', - search: { debug: 'true'}, + search: { debug: 'true' }, href: 'ws://foo:123/onos/ui/websock/path', absUrl: 'ws://foo:123/onos/ui/websock/path' } @@ -127,7 +128,8 @@ describe('AppsComponent', () => { { provide: HttpClient, useClass: MockHttpClient }, { provide: IconService, useClass: MockIconService }, { provide: KeyService, useClass: MockKeyService }, - { provide: LionService, useFactory: (() => { + { + provide: LionService, useFactory: (() => { return { bundle: ((bundleId) => mockLion), ubercache: new Array(), @@ -143,7 +145,7 @@ describe('AppsComponent', () => { { provide: 'Window', useValue: windowMock }, ] }) - .compileComponents(); + .compileComponents(); logServiceSpy = TestBed.get(LogService); })); 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 ffd7b37a23..b2f62311fb 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 @@ export class AppsComponent extends TableBaseImpl implements OnInit, OnDestroy { evt.preventDefault(); evt.stopPropagation(); } + + deselectRow(event) { + this.log.debug('Details panel close event'); + this.selId = event; + this.ctrlBtnState = { + 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 13f1847a21..a705d45c63 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 @@ div.dropping { #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 ce9af3a121..90d4b1440e 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 a78bdd9f61..d43c26407d 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 @@ -
-
-

Devices ({{ tableData.length }} total)

-
-
- - -
-
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
-
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Friendly Name Device ID Master Ports Vendor H/W Version S/W Version Protocol
{{ annots.noRowsMsg }}
- - - - - {{ dev.name }}{{ dev.id }}{{ dev.masterid }}{{ dev.num_ports }}{{ dev.mfr }}{{ dev.hw }}{{ dev.sw }}{{ dev.protocol }}
-
- -

TODO (21 Jun 18): Add in:

-
    -
  • Scrolling for long lists of devices
  • -
  • Sorting by column
  • -
  • Left align header columns
  • -
  • Move tooltip to underneath icon
  • -
  • Correct width and icon colour of active and device icon columns
  • -
  • Add device details panel
  • -
  • Add more unit tests
  • -
  • Make icon for #undefined work (e.g. for device type olt or unknown)
  • -
  • Change loading service to fade in and out and have a threshold of
  • -
-
-
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 7840292f93..9ad545613b 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.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 4d8454df54..5ed16e8219 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 0000000000..7a22076eae --- /dev/null +++ b/web/gui2/src/main/webapp/app/view/device/device/device.component.html @@ -0,0 +1,121 @@ + +
+
+

Devices ({{ tableData.length }} total)

+
+
+ + +
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+ +
+ +
+
+ + + + + + + + + + + + + +
Friendly Name + + Device ID + + Master + + Ports + + Vendor + + H/W Version + + S/W Version + + Protocol + +
+
+
+ + + + + + + + + + + + + + + + +
{{ annots.noRowsMsg }}
+ + + + {{ dev.name }}{{ dev.id }}{{ dev.masterid }}{{ dev.num_ports }}{{ dev.mfr }}{{ dev.hw }}{{ dev.sw }}{{ dev.protocol }}
+
+
+ + +
\ No newline at end of file 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/device.component.spec.ts similarity index 55% rename from web/gui2/src/main/webapp/app/view/device/device.component.spec.ts rename to web/gui2/src/main/webapp/app/view/device/device/device.component.spec.ts index 066a636e58..207dde9399 100644 --- a/web/gui2/src/main/webapp/app/view/device/device.component.spec.ts +++ b/web/gui2/src/main/webapp/app/view/device/device/device.component.spec.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. @@ -17,21 +17,26 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, Params } from '@angular/router'; import { DebugElement } from '@angular/core'; import { By } from '@angular/platform-browser'; -import { LogService } from '../../log.service'; +import { LogService } from '../../../log.service'; import { DeviceComponent } from './device.component'; +import { } from 'jasmine'; -import { FnService, WindowSize } from '../../fw/util/fn.service'; -import { IconService } from '../../fw/svg/icon.service'; -import { GlyphService } from '../../fw/svg/glyph.service'; -import { IconComponent } from '../../fw/svg/icon/icon.component'; -import { KeyService } from '../../fw/util/key.service'; -import { LoadingService } from '../../fw/layer/loading.service'; -import { NavService } from '../../fw/nav/nav.service'; -import { MastService } from '../../fw/mast/mast.service'; -import { SvgUtilService } from '../../fw/svg/svgutil.service'; -import { ThemeService } from '../../fw/util/theme.service'; -import { WebSocketService } from '../../fw/remote/websocket.service'; +import { FnService } from '../../../fw/util/fn.service'; +import { IconService } from '../../../fw/svg/icon.service'; +import { GlyphService } from '../../../fw/svg/glyph.service'; +import { IconComponent } from '../../../fw/svg/icon/icon.component'; +import { KeyService } from '../../../fw/util/key.service'; +import { LoadingService } from '../../../fw/layer/loading.service'; +import { NavService } from '../../../fw/nav/nav.service'; +import { MastService } from '../../../fw/mast/mast.service'; +import { TableFilterPipe } from '../../../fw/widget/tablefilter.pipe'; +import { ThemeService } from '../../../fw/util/theme.service'; +import { WebSocketService } from '../../../fw/remote/websocket.service'; import { of } from 'rxjs'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { FormsModule } from '@angular/forms'; +import { DeviceDetailsComponent } from './../devicedetails/devicedetails.component'; +import { RouterTestingModule } from '@angular/router/testing'; class MockActivatedRoute extends ActivatedRoute { constructor(params: Params) { @@ -40,38 +45,30 @@ class MockActivatedRoute extends ActivatedRoute { } } -class MockDetailsPanelService {} - -class MockFnService {} - class MockIconService { - loadIconDef() {} + loadIconDef() { } } -class MockGlyphService {} +class MockGlyphService { } -class MockKeyService {} +class MockKeyService { } class MockLoadingService { - startAnim() {} - stop() {} + startAnim() { } + stop() { } } -class MockNavService {} +class MockNavService { } -class MockMastService {} +class MockMastService { } -class MockTableBuilderService {} - -class MockTableDetailService {} - -class MockThemeService {} +class MockThemeService { } class MockWebSocketService { - createWebSocket() {} + createWebSocket() { } isConnected() { return false; } - unbindHandlers() {} - bindHandlers() {} + unbindHandlers() { } + bindHandlers() { } } /** @@ -87,15 +84,15 @@ describe('DeviceComponent', () => { beforeEach(async(() => { const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']); - ar = new MockActivatedRoute({'debug': 'txrx'}); + ar = new MockActivatedRoute({ 'debug': 'txrx' }); windowMock = { - location: { + location: { hostname: 'foo', host: 'foo', port: '80', protocol: 'http', - search: { debug: 'true'}, + search: { debug: 'true' }, href: 'ws://foo:123/onos/ui/websock/path', absUrl: 'ws://foo:123/onos/ui/websock/path' } @@ -103,7 +100,8 @@ describe('DeviceComponent', () => { fs = new FnService(ar, logSpy, windowMock); TestBed.configureTestingModule({ - declarations: [ DeviceComponent, IconComponent ], + imports: [BrowserAnimationsModule, FormsModule, RouterTestingModule], + declarations: [DeviceComponent, IconComponent, TableFilterPipe, DeviceDetailsComponent], providers: [ { provide: FnService, useValue: fs }, { provide: IconService, useClass: MockIconService }, @@ -116,9 +114,8 @@ describe('DeviceComponent', () => { { provide: ThemeService, useClass: MockThemeService }, { provide: WebSocketService, useClass: MockWebSocketService }, { provide: 'Window', useValue: windowMock }, - ] - }) - .compileComponents(); + ] + }).compileComponents(); logServiceSpy = TestBed.get(LogService); })); @@ -132,10 +129,29 @@ describe('DeviceComponent', () => { expect(component).toBeTruthy(); }); + it('should have a div.tabular-header inside a div#ov-device', () => { + const devDe: DebugElement = fixture.debugElement; + const divDe = devDe.query(By.css('div#ov-device div.tabular-header')); + expect(divDe).toBeTruthy(); + }); + it('should have .table-header with "Friendly Name..."', () => { - const appDe: DebugElement = fixture.debugElement; - const divDe = appDe.query(By.css('.table-header')); + const devDe: DebugElement = fixture.debugElement; + const divDe = devDe.query(By.css('div#ov-device div.table-header')); const div: HTMLElement = divDe.nativeElement; expect(div.textContent).toEqual('Friendly Name Device ID Master Ports Vendor H/W Version S/W Version Protocol '); }); + + it('should have a refresh button inside the div.tabular-header', () => { + const devDe: DebugElement = fixture.debugElement; + const divDe = devDe.query(By.css('div#ov-device div.tabular-header div.ctrl-btns div.refresh')); + expect(divDe).toBeTruthy(); + }); + + + it('should have a div.table-body ', () => { + const devDe: DebugElement = fixture.debugElement; + const divDe = devDe.query(By.css('div#ov-device div.table-body')); + expect(divDe).toBeTruthy(); + }); }); 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 ecccc348ae..b6f1d95d68 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 @@ interface Device { * ONOS GUI -- Device View Component */ @Component({ - selector: 'onos-device', - templateUrl: './device.component.html', - styleUrls: ['./device.component.css', './device.theme.css', '../../fw/widget/table.css', '../../fw/widget/table.theme.css'] + selector: 'onos-device', + templateUrl: './device.component.html', + styleUrls: ['./device.component.css', './device.theme.css', '../../../fw/widget/table.css', '../../../fw/widget/table.theme.css'] }) export class DeviceComponent extends TableBaseImpl implements OnInit, OnDestroy { @@ -71,16 +68,29 @@ export class DeviceComponent extends TableBaseImpl implements OnInit, OnDestroy constructor( protected fs: FnService, protected ls: LoadingService, - private is: IconService, - private ks: KeyService, protected log: LogService, - private mast: MastService, - private nav: NavService, + protected as: ActivatedRoute, + protected router: Router, protected wss: WebSocketService, - @Inject('Window') private window: Window, ) { super(fs, ls, log, wss, 'device'); this.responseCallback = this.deviceResponseCb; + + this.as.queryParams.subscribe(params => { + this.selId = params['devId']; + + }); + + this.payloadParams = { + devId: this.selId + }; + + this.sortParams = { + firstCol: 'name', + firstDir: SortDir.asc, + secondCol: 'id', + secondDir: SortDir.desc, + }; } ngOnInit() { @@ -97,4 +107,11 @@ export class DeviceComponent extends TableBaseImpl implements OnInit, OnDestroy this.log.debug('Device response received for ', data.devices.length, 'devices'); } + navto(path) { + this.log.debug('navigate'); + if (this.selId) { + this.router.navigate([path], { queryParams: { devId: this.selId } }); + } + } + } diff --git a/web/gui2/src/main/webapp/app/view/device/device.theme.css b/web/gui2/src/main/webapp/app/view/device/device/device.theme.css similarity index 58% rename from web/gui2/src/main/webapp/app/view/device/device.theme.css rename to web/gui2/src/main/webapp/app/view/device/device/device.theme.css index df0f139279..d9b2c07228 100644 --- a/web/gui2/src/main/webapp/app/view/device/device.theme.css +++ b/web/gui2/src/main/webapp/app/view/device/device/device.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. @@ -28,4 +28,33 @@ .light #device-details-panel .bottom tr:nth-child(even) { background-color: #f4f4f4; } +#ov-device .tabular-header { + text-align: left; +} +#ov-device div.summary-list .table-header td { + font-weight: bold; + font-variant: small-caps; + text-transform: uppercase; + font-size: 10pt; + padding-top: 8px; + padding-bottom: 8px; + letter-spacing: 0.02em; + cursor: pointer; + background-color: #e5e5e6; + color: #3c3a3a; +} +#ov-device div.summary-list .table-body { + overflow:scroll; +} +#ov-device h2 { + display: inline-block; +} + +#ov-device, div.ctrl-btns { +} + +#ov-device th, td { + text-align: left; + padding: 8px; +} diff --git a/web/gui2/src/main/webapp/app/view/device/devicedetails/devicedetails.component.css b/web/gui2/src/main/webapp/app/view/device/devicedetails/devicedetails.component.css new file mode 100644 index 0000000000..a3903b0804 --- /dev/null +++ b/web/gui2/src/main/webapp/app/view/device/devicedetails/devicedetails.component.css @@ -0,0 +1,114 @@ +/* + * Copyright 2018-present Open Networking Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#device-details-panel.floatpanel { + z-index: 0; + font-size: 10pt; + top: 185px; +} + +#device-details-panel.floatpanel a { + font-weight: bold; +} + +#device-details-panel .container { + padding: 0 30px; +} + +#device-details-panel .close-btn { + position: absolute; + right:5px; + top: 5px; + cursor: pointer; +} + +#device-details-panel .dev-icon { + display: inline-block; + padding: 0 6px 0 0; + vertical-align: middle; +} + +#device-details-panel h2 { + display: inline-block; + margin: 8px 0; + font-weight: bold; + font-size: 16pt; +} + +#device-details-panel h2 input { + font-size: 0.90em; + width: 106%; +} + +#device-details-panel .actionBtns div { + padding: 12px 6px; +} + +#device-details-panel hr { + width: 100%; + margin: 2px auto; +} + +#device-details-panel .top-tables { + font-size: 10pt; + white-space: nowrap; +} + +#device-details-panel td.label { + font-weight: bold; + text-align: right; + padding-right: 6px; +} + +#device-details-panel .bottom table { + border-spacing: 0; + height: 358px; + width: 520px; + overflow: auto; + display: block; +} + +#device-details-panel .bottom th { + letter-spacing: 0.02em; +} + +#device-details-panel .bottom th, +#device-details-panel .bottom td { + padding: 6px 12px; + text-align: center; +} + +#device-details-panel .top div.left { + float: left; + text-align: left; + padding: 0 10px 0 0; +} + +#device-details-panel .top div.right { + display: inline-block; +} + +#device-details-panel .editable { + border-bottom: 1px dashed #ca504b; +} + +#device-details-panel .clickable { + cursor: pointer; +} + +#device-details-panel .bottom thead tr { + background-color: #e5e5e6; +} \ 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 0000000000..034b63b77f --- /dev/null +++ b/web/gui2/src/main/webapp/app/view/device/devicedetails/devicedetails.component.html @@ -0,0 +1,111 @@ + + +
+
+
+
+ +
+
+ +
+

{{detailsData.id}}

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
URI :{{detailsData.id}}
Type :{{detailsData.type}}
Master ID :{{detailsData.masterid}}
Chassis ID :{{detailsData.chassisid}}
Vendor :{{detailsData.mfr}}
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
H/W Version :{{detailsData.hw}}
S/W Version :{{detailsData.sw}}
Protocol :{{detailsData.protocol}}
Serial # :{{detailsData.serial}}
Pipeconf :{{detailsData.pipeconf}}
+
+
+
+
+
+
+

Ports

+ + + + + + + + + + + + + + + + + + + + + +
EnabledIDSpeedTypeEgress LinksName
{{port.enabled}}{{port.id}}{{port.speed}}{{port.type}}{{port.elinks_dest}}{{port.name}}
+
+
+
\ 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 0000000000..5daa2efc20 --- /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; + let component: DeviceDetailsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']); + ar = new MockActivatedRoute({ 'debug': 'panel' }); + windowMock = { + location: { + 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 0000000000..9e30eb74dc --- /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 d81a67d6fe..0000000000 --- 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 0000000000..4ddf65fa96 --- /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 0000000000..55d3c96f99 --- /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 0000000000..3ed4d1678a --- /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 0000000000..a1b23dfe6c --- /dev/null +++ b/web/gui2/src/main/webapp/app/view/flow/flow/flow.component.html @@ -0,0 +1,136 @@ + +
+
+

+ {{lionFn('title_flows')}} {{id}} ({{ tableData.length }} {{ lionFn('total') }}) +

+
+
+ + +
+
+ +
+ +
+
+ + +
+ +
+
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+ +
+ +
+
+ + + + + + + + + + + +
{{lionFn('state')}} + + {{lionFn('packets')}} + + {{lionFn('duration')}} + + {{lionFn('priority')}} + + {{lionFn('tableName')}} + + {{lionFn('selector')}} + + {{lionFn('treatment')}} + + {{lionFn('appName')}} + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + +
{{annots.noRowsMsg}}
{{flow.state}}{{flow.packets}}{{flow.duration}}{{flow.priority}}{{flow.tableName}}{{flow.selector_c}}{{flow.treatment_c}}{{flow.appName}}
{{flow.selector}}
{{flow.treatment}}
+
+
+ +
\ 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 0000000000..e6ed12a918 --- /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; + let component: FlowComponent; + let fixture: ComponentFixture; + + 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 = { + location: { + 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 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 0000000000..96692a4302 --- /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 0000000000..37738a98ad --- /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 0000000000..2f113491a9 --- /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 0000000000..05d0a47f57 --- /dev/null +++ b/web/gui2/src/main/webapp/app/view/flow/flowdetails/flowdetails/flowdetails.component.html @@ -0,0 +1,116 @@ + +
+
+
+
+ +
+
+ +
+

{{ flowId }}

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ lionFn('flowId') }} :{{ flowId }}
{{ lionFn('state') }} :{{ detailsData.state }}
{{ lionFn('bytes') }} :{{ detailsData.bytes }}
{{ lionFn('packets') }} :{{ detailsData.packets }}
{{ lionFn('duration') }} :{{ detailsData.duration }}
{{ lionFn('priority') }} :{{ detailsData.priority }}
{{ lionFn('tableName') }} :{{ detailsData.tableName }}
{{ lionFn('appName') }} :{{ detailsData.appName }}
{{ lionFn('appId') }} :{{ detailsData.appId }}
{{ lionFn('groupId') }} :{{ detailsData.groupId }}
{{ lionFn('idleTimeout') }} :{{ detailsData.idleTimeout }}
{{ lionFn('hardTimeout') }} :{{ detailsData.hardTimeout }}
{{ lionFn('permanent') }} :{{ detailsData.permanent }}
+
+
+

{{ lionFn('selector') }}

+
+ + + + + + + +
ETH_TYPE :{{ detailsData.selector }}
+
+
+

{{ lionFn('treatment') }}

+
+ + + + + + + + + + + +
[imm]OUTPUT :{{ immed(detailsData.treatment) }}
Clear deferred :{{ clearDef(detailsData.treatment) }}
+
+
+
+
+
\ 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 0000000000..8bbf482445 --- /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; + let component: FlowDetailsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']); + ar = new MockActivatedRoute({ 'debug': 'panel' }); + windowMock = { + location: { + 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 0000000000..3197285a7c --- /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(); + + 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 0000000000..9ce3153955 --- /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 0000000000..343a971e3c --- /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 0000000000..fbf19def5b --- /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 0000000000..4b0afbb2fb --- /dev/null +++ b/web/gui2/src/main/webapp/app/view/group/group/group.component.html @@ -0,0 +1,124 @@ + +
+
+

+ Groups for Device {{id}} ({{tableData.length}} total) +

+
+
+ + +
+
+ +
+ +
+
+ + +
+ +
+
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+ + +
+ +
+
+ + + + + + + + + +
Group Id + + App Id + + State + + Type + + Packets + + Bytes + +
+
+ +
+ + + + + + + + + + + + + + + + + +
{{ annots.noRowsMsg }}
{{group.id}}{{group.app_id}}{{group.state}}{{group.type}}{{group.packets}}{{group.bytes}}
+
+
+
\ 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 0000000000..bcf0ca2007 --- /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; + 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 = { + location: { + 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 0000000000..fc6e9bd5da --- /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 0000000000..f90556c99b --- /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 0000000000..62f10a18cf --- /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 0000000000..230b55a7dc --- /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 0000000000..d306a231d7 --- /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 0000000000..7c7e32c761 --- /dev/null +++ b/web/gui2/src/main/webapp/app/view/host/host/host.component.html @@ -0,0 +1,76 @@ + +
+
+

Hosts ({{tableData.length}} total)

+
+
+ + +
+
+
+
+
+ + + + + + + + + + + +
Friendly Name + + Host ID + + MAC Address + + VLAN ID + + Configured + + IP Addresses + + Location + +
+
+
+ + + + + + + + + + + + + + +
{{ annots.noRowsMsg }}
+ + {{host.name}}{{host.id}}{{host.mac}}{{host.vlan}}{{host.configured}}{{host.ips}}{{host.location}}
+
+
+ +
\ 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 0000000000..6d915c737f --- /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; + let component: HostComponent; + let fixture: ComponentFixture; + 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 = { + location: { + 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 0000000000..c13e2f50e6 --- /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 0000000000..39b01316a4 --- /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 0000000000..c7c9dbacb0 --- /dev/null +++ b/web/gui2/src/main/webapp/app/view/host/hostdetails/hostdetails.component.html @@ -0,0 +1,70 @@ + +
+
+
+ +
+
+ +
+

{{detailsData.name}}

+
+
+
+ + + + + + + + + + + + + + + +
Host ID :{{detailsData.id}}
IP Address :{{detailsData.ips}}
MAC Address :{{detailsData.mac}}
+
+
+ + + + + + + + + + + + + + + +
VLAN :{{detailsData.vlan}}
Configured :{{detailsData.configured}}
Location :{{detailsData.location}}
+
+
+
+
+
+ +
+
\ 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 0000000000..0ce1ba09bf --- /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; + let component: HostDetailsComponent; + let fixture: ComponentFixture; + + 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 = { + location: { + 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 0000000000..7d1132d5ca --- /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 0000000000..a2ff939364 --- /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 0000000000..92276b0cc3 --- /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 0000000000..a7c04e2475 --- /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 0000000000..e90ede83e9 --- /dev/null +++ b/web/gui2/src/main/webapp/app/view/link/link/link.component.html @@ -0,0 +1,66 @@ + + \ 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 0000000000..699e535015 --- /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; + let component: LinkComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']); + ar = new MockActivatedRoute({ 'debug': 'txrx' }); + + windowMock = { + location: { + 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 0000000000..eeb7c33b0d --- /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 0000000000..83a432e371 --- /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 0000000000..90263b2ad5 --- /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 0000000000..809ab49385 --- /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 0000000000..bfeb40743f --- /dev/null +++ b/web/gui2/src/main/webapp/app/view/meter/meter/meter.component.html @@ -0,0 +1,101 @@ + +
+
+

Meter for Device {{id}} ({{tableData.length}} Total )

+ +
+
+ + +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+
+ + + + + + + + +
Meter ID + + App ID + + State + + Packets + + + Bytes + +
+
+ +
+ + + + + + + + + + + + + + + + +
{{annots.noRowsMsg}}
{{meter.id}}{{meter.app_id}}{{meter.state}}{{meter.packets}}{{meter.bytes}}
+
+
+
\ 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 0000000000..e17ff50d1e --- /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; + let component: MeterComponent; + let fixture: ComponentFixture; + + 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 = { + location: { + 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 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 0000000000..e026e1ca6a --- /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 0000000000..9f4dea4dae --- /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 0000000000..6ba681d23a --- /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 0000000000..dd622dc602 --- /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 0000000000..015d47596f --- /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 0000000000..8625d1a069 --- /dev/null +++ b/web/gui2/src/main/webapp/app/view/port/port/port.component.html @@ -0,0 +1,136 @@ + + +
+
+

+ Ports for Device {{devId}} ({{tableData.length}} Total) +

+ +
+
+ + +
+ +
+ +
+ + +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+ + +
+ +
+
+ + + + + + + + + + + +
Port ID + + Pkts Received + + Pkts Sent + + Bytes Received + + Bytes Sent + + Pkts RX Dropped + + Pkts TX Dropped + + Duration (sec) + +
+
+ +
+ + + + + + + + + + + + + + + +
{{annots.noRowsMsg}}
{{port.id}}{{port.pkt_rx}}{{port.pkt_tx}}{{port.bytes_rx}}{{port.bytes_tx}}{{port.pkt_rx_drp}}{{port.pkt_tx_drp}}{{port.duration}}
+
+ +
+
\ 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 0000000000..233b34ef34 --- /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; + let component: PortComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']); + ar = new MockActivatedRoute({ 'debug': 'txrx' }); + + windowMock = { + location: { + 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 0000000000..1c86c9c56c --- /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 0000000000..225679d314 --- /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 0000000000..36fa847be4 --- /dev/null +++ b/web/gui2/src/main/webapp/app/view/port/portdetails/portdetails.component.html @@ -0,0 +1,60 @@ + +
+
+
+
+ +
+
+ +
+

{{detailsData.devId}} port {{detailsData.id}}

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
ID :{{detailsData.id}}
Device :{{detailsData.devId}}
Type :{{detailsData.type}}
Speed :{{detailsData.speed}}
Enabled :{{detailsData.enabled}}
+
+
+
+
+
+
+
+
\ 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 0000000000..18dc37bb9d --- /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; + let component: PortDetailsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']); + ar = new MockActivatedRoute({ 'debug': 'panel' }); + windowMock = { + location: { + 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 0000000000..1fd62d3293 --- /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 0000000000..dbbee7fe23 --- /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 0000000000..694f645464 --- /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 0000000000..56e03b6b1c --- /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 0000000000..73472ffd3b --- /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 0000000000..5829acf02c --- /dev/null +++ b/web/gui2/src/main/webapp/app/view/tunnel/tunnel/tunnel.component.html @@ -0,0 +1,74 @@ + +
+
+

Tunnels ({{tableData.length}} total)

+
+
+ +
+
+
+
+
+ + + + + + + + + + + +
Id + + Name + + Port 1 + + Port 2 + + Type + + Group Id + + Bandwidth + + Path + +
+
+
+ + + + + + + + + + + + + + +
{{ annots.noRowsMsg }}
{{tunnel.id}}{{tunnel.name}}{{tunnel.one}}{{tunnel.two}}{{tunnel.type}}{{tunnel.group_id}}{{tunnel.bandwidth}}{{tunnel.path}}
+
+
+
\ 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 0000000000..3b2611eaeb --- /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; + let component: TunnelComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']); + ar = new MockActivatedRoute({ 'debug': 'txrx' }); + + windowMock = { + location: { + 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 0000000000..bbd4b6769f --- /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'); + } + +}