[ONOS-6964][ONOS-6966] Add pipeconf codec and pipeconf view

Change-Id: Ie60a5451bcc24a27ede655c8230d82998ea4f3be
diff --git a/web/gui/src/main/webapp/app/view/device/device.html b/web/gui/src/main/webapp/app/view/device/device.html
index ac0febf..c26e067 100644
--- a/web/gui/src/main/webapp/app/view/device/device.html
+++ b/web/gui/src/main/webapp/app/view/device/device.html
@@ -31,6 +31,11 @@
                  icon icon-id="meterTable" icon-size="42"
                  tooltip tt-msg="meterTip"
                  ng-click="nav('meter')"></div>
+
+            <div ng-class="{active: !!selId}"
+                 icon icon-id="pipeconfTable" icon-size="42"
+                 tooltip tt-msg="pipeconfTip"
+                 ng-click="nav('pipeconf')"></div>
         </div>
     </div>
 
diff --git a/web/gui/src/main/webapp/app/view/device/device.js b/web/gui/src/main/webapp/app/view/device/device.js
index 6bc66d8..536b789 100644
--- a/web/gui/src/main/webapp/app/view/device/device.js
+++ b/web/gui/src/main/webapp/app/view/device/device.js
@@ -229,6 +229,7 @@
             $scope.portTip = 'Show port view for selected device';
             $scope.groupTip = 'Show group view for selected device';
             $scope.meterTip = 'Show meter view for selected device';
+            $scope.pipeconfTip = 'Show pipeconf view for selected device';
 
             // details panel handlers
             // handlers[detailsResp] = respDetailsCb;
diff --git a/web/gui/src/main/webapp/app/view/flow/flow.html b/web/gui/src/main/webapp/app/view/flow/flow.html
index 3039983..a6fed1a 100644
--- a/web/gui/src/main/webapp/app/view/flow/flow.html
+++ b/web/gui/src/main/webapp/app/view/flow/flow.html
@@ -53,6 +53,11 @@
                  tooltip tt-msg="meterTip"
                  ng-click="nav('meter')"></div>
 
+            <div class="active"
+                 icon icon-id="pipeconfTable" icon-size="42"
+                 tooltip tt-msg="pipeconfTip"
+                 ng-click="nav('pipeconf')"></div>
+
         </div>
 
         <div class="search">
diff --git a/web/gui/src/main/webapp/app/view/flow/flow.js b/web/gui/src/main/webapp/app/view/flow/flow.js
index cd1e58a..71794bd 100644
--- a/web/gui/src/main/webapp/app/view/flow/flow.js
+++ b/web/gui/src/main/webapp/app/view/flow/flow.js
@@ -246,8 +246,10 @@
                     $scope.portTip = 'Show port view for this device';
                     $scope.groupTip = 'Show group view for this device';
                     $scope.meterTip = 'Show meter view for selected device';
+                    $scope.pipeconfTip = 'Show pipeconf view for selected device';
                     $scope.briefTip = 'Switch to brief view';
                     $scope.detailTip = 'Switch to detailed view';
+
                     $scope.brief = true;
                     params = $location.search();
                     if (params.hasOwnProperty('devId')) {
diff --git a/web/gui/src/main/webapp/app/view/group/group.html b/web/gui/src/main/webapp/app/view/group/group.html
index a09dd18..fb6b277 100644
--- a/web/gui/src/main/webapp/app/view/group/group.html
+++ b/web/gui/src/main/webapp/app/view/group/group.html
@@ -52,6 +52,11 @@
                  icon icon-id="meterTable" icon-size="42"
                  tooltip tt-msg="meterTip"
                  ng-click="nav('meter')"></div>
+
+            <div class="active"
+                 icon icon-id="pipeconfTable" icon-size="42"
+                 tooltip tt-msg="pipeconfTip"
+                 ng-click="nav('pipeconf')"></div>
         </div>
 
         <div class="search">
diff --git a/web/gui/src/main/webapp/app/view/group/group.js b/web/gui/src/main/webapp/app/view/group/group.js
index 063daef..0b36d2c 100644
--- a/web/gui/src/main/webapp/app/view/group/group.js
+++ b/web/gui/src/main/webapp/app/view/group/group.js
@@ -41,6 +41,7 @@
             $scope.flowTip = 'Show flow view for this device';
             $scope.portTip = 'Show port view for this device';
             $scope.meterTip = 'Show meter view for selected device';
+            $scope.pipeconfTip = 'Show pipeconf view for selected device';
             $scope.briefTip = 'Switch to brief view';
             $scope.detailTip = 'Switch to detailed view';
             $scope.brief = true;
diff --git a/web/gui/src/main/webapp/app/view/meter/meter.html b/web/gui/src/main/webapp/app/view/meter/meter.html
index cd83392..ddd17f0 100644
--- a/web/gui/src/main/webapp/app/view/meter/meter.html
+++ b/web/gui/src/main/webapp/app/view/meter/meter.html
@@ -36,6 +36,11 @@
 
             <div class="current-view"
                  icon icon-id="meterTable" icon-size="42"></div>
+
+            <div class="active"
+                 icon icon-id="pipeconfTable" icon-size="42"
+                 tooltip tt-msg="pipeconfTip"
+                 ng-click="nav('pipeconf')"></div>
         </div>
 
         <div class="search">
diff --git a/web/gui/src/main/webapp/app/view/meter/meter.js b/web/gui/src/main/webapp/app/view/meter/meter.js
index bec7d2e..32058b6 100644
--- a/web/gui/src/main/webapp/app/view/meter/meter.js
+++ b/web/gui/src/main/webapp/app/view/meter/meter.js
@@ -40,6 +40,7 @@
             $scope.flowTip = 'Show flow view for this device';
             $scope.portTip = 'Show port view for this device';
             $scope.groupTip = 'Show group view for this device';
+            $scope.pipeconfTip = 'Show pipeconf view for selected device';
 
             params = $location.search();
             if (params.hasOwnProperty('devId')) {
diff --git a/web/gui/src/main/webapp/app/view/pipeconf/pipeconf.css b/web/gui/src/main/webapp/app/view/pipeconf/pipeconf.css
new file mode 100644
index 0000000..81eb603
--- /dev/null
+++ b/web/gui/src/main/webapp/app/view/pipeconf/pipeconf.css
@@ -0,0 +1,163 @@
+/*
+ * Copyright 2017-present Open 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.
+ */
+
+/* Base */
+#pipeconf-info h2 {
+    display: inline-block;
+    margin: 10px 0 10px 0;
+}
+
+#pipeconf-info h3 {
+    display: inline-block;
+    margin-bottom: 10px;
+}
+
+#pipeconf-info {
+    height: inherit;
+    overflow-y: scroll;
+    overflow-x: hidden;
+    padding-left: 16px;
+    padding-right: 20px;
+}
+
+#pipeconf-info::-webkit-scrollbar {
+    display: none;
+}
+
+/* Table */
+.pipeconf-table {
+    width: 100%;
+}
+
+.pipeconf-table tr {
+    transition: background-color 500ms;
+    text-align: left;
+    padding: 8px 4px;
+}
+
+.pipeconf-table .selected {
+    background-color: #dbeffc !important;
+}
+
+.pipeconf-table tr:nth-child(even) {
+    background-color: #f4f4f4;
+}
+
+.pipeconf-table tr:nth-child(odd) {
+    background-color: #fbfbfb;
+}
+
+.pipeconf-table tr th {
+    background-color: #e5e5e6;
+    color: #3c3a3a;
+    font-weight: bold;
+    font-variant: small-caps;
+    text-transform: uppercase;
+    font-size: 10pt;
+    letter-spacing: 0.02em;
+}
+
+.pipeconf-table tr td {
+    padding: 4px;
+    text-align: left;
+    word-wrap: break-word;
+    font-size: 10pt;
+}
+
+.pipeconf-table tr td p {
+    margin: 5px 0;
+}
+
+/* Detail panel */
+.container {
+    padding: 10px;
+    overflow: hidden;
+}
+
+.container .top, .bottom {
+    padding: 15px;
+}
+.container .bottom {
+    overflow-y: scroll;
+}
+
+.container .bottom h2 {
+    margin: 0;
+}
+
+.container .top .detail-panel-header {
+    display: inline-block;
+    margin: 0 0 10px;
+}
+
+
+.detail-panel-table {
+    width: 100%;
+    overflow-y: hidden;
+}
+
+.detail-panel-table td, th {
+    text-align: left;
+    padding: 6px 12px;
+}
+
+.top-info {
+    font-size: 12pt;
+}
+
+.top-info .label {
+    font-weight: bold;
+    text-align: right;
+    display: inline-block;
+    margin: 0;
+    padding-right:6px;
+}
+
+.top-info .value {
+    margin: 0;
+    text-align: left;
+    display: inline-block;
+}
+
+/* Widgets */
+#ov-pipeconf h2 {
+    display: inline-block;
+}
+
+.collapse-btn {
+    cursor: pointer;
+    display: inline-block;
+    max-height: 30px;
+    overflow-y: hidden;
+    position: relative;
+    top: 8px;
+}
+
+.close-btn {
+    display: inline-block;
+    float: right;
+    margin: 0.1em;
+    cursor: pointer;
+}
+
+.text-center {
+    text-align: center !important;
+}
+
+.no-data {
+    text-align: center !important;
+    font-style: italic;
+}
diff --git a/web/gui/src/main/webapp/app/view/pipeconf/pipeconf.html b/web/gui/src/main/webapp/app/view/pipeconf/pipeconf.html
new file mode 100644
index 0000000..4d4e8f6
--- /dev/null
+++ b/web/gui/src/main/webapp/app/view/pipeconf/pipeconf.html
@@ -0,0 +1,204 @@
+<!--
+  ~ Copyright 2017-present Open Networking Foundation
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<div id="ov-pipeconf">
+    <div class="tabular-header">
+        <h2>
+            Pipeconf for Device {{devId || "(No device selected)"}}
+        </h2>
+
+        <div class="ctrl-btns">
+            <div class="refresh" ng-class="{active: autoRefresh}"
+                 icon icon-size="42" icon-id="refresh"
+                 tooltip tt-msg="autoRefreshTip"
+                 ng-click="autoRefresh = !autoRefresh"></div>
+
+            <div class="separator"></div>
+
+            <div class="active"
+                 icon icon-id="deviceTable" icon-size="42"
+                 tooltip tt-msg="deviceTip"
+                 ng-click="nav('device')"></div>
+
+            <div class="active"
+                 icon icon-id="flowTable" icon-size="42"
+                 tooltip tt-msg="flowTip"
+                 ng-click="nav('flow')"></div>
+
+            <div class="active"
+                 icon icon-id="portTable" icon-size="42"
+                 tooltip tt-msg="portTip"
+                 ng-click="nav('port')"></div>
+
+            <div class="active"
+                 icon icon-id="groupTable" icon-size="42"
+                 tooltip tt-msg="groupTip"
+                 ng-click="nav('group')"></div>
+
+            <div class="active"
+                 icon icon-id="meterTable" icon-size="42"
+                 tooltip tt-msg="meterTip"
+                 ng-click="nav('meter')"></div>
+
+            <div class="current-view"
+                 icon icon-id="pipeconfTable" icon-size="42"
+                 tooltip tt-msg="pipeconfTip"></div>
+        </div>
+    </div>
+    <div id="pipeconf-info" auto-height>
+        <div id="pipeconf-basic">
+            <h2>Basic information</h2>
+            <table class="pipeconf-table">
+                <tr>
+                    <th class="text-center" style="width: 160px">Name</th>
+                    <th>Info</th>
+                </tr>
+                <tr ng-show="collapsePipeconf">
+                    <td colspan="2" class="text-center">....</td>
+                </tr>
+                <tr ng-show="pipeconf === null">
+                    <td colspan="2" class="no-data">
+                        No PiPipeconf for this device
+                    </td>
+                </tr>
+                <tr ng-show-start="!collapsePipeconf && pipeconf !== null">
+                    <td class="text-center">ID</td>
+                    <td>{{pipeconf.id}}</td>
+                </tr>
+                <tr>
+                    <td class="text-center">Behaviors</td>
+                    <td>{{pipeconf.behaviors.join(", ")}}</td>
+                </tr>
+                <tr ng-show-end>
+                    <td class="text-center">Extensions</td>
+                    <td>{{pipeconf.extensions.join(", ")}}</td>
+                </tr>
+            </table>
+        </div>
+        <!-- ng-show-start for checking pipeconf !== null -->
+        <h2 ng-show-start="pipeconf !== null">Pipeline Model</h2>
+        <div id="pipeconf-headers">
+            <h3>Headers</h3>
+            <div ng-show="!collapseHeaders"
+                 class="collapse-btn" icon icon-id="plus" icon-size="30"
+                 ng-click="collapseHeaders = !collapseHeaders"></div>
+            <div ng-show="collapseHeaders"
+                 class="collapse-btn" icon icon-id="minus" icon-size="30"
+                 ng-click="collapseHeaders = !collapseHeaders"></div>
+            <table class="pipeconf-table">
+                <tr>
+                    <th style="width: 160px">Name</th>
+                    <th class="text-center" style="width: 100px">Is metadata</th>
+                    <th class="text-center" style="width: 100px">Index</th>
+                    <th>Header type</th>
+                </tr>
+                <tr ng-show="collapseHeaders">
+                    <td colspan="4" class="clickable text-center"
+                        ng-click="collapseHeaders = !collapseHeaders">....</td>
+                </tr>
+                <tr ng-show="!collapseHeaders && pipelineModel.headers.length === 0">
+                    <td colspan="4" class="no-data">No Data</td>
+                </tr>
+                <tr ng-show="!collapseHeaders"
+                    ng-repeat="header in pipelineModel.headers"
+                    ng-click="headerSelectCb($event, header)"
+                    ng-class="{selected: header.name === selectedId.name && selectedId.type === 'header'}"
+                    class="clickable">
+                    <td>{{header.name}}</td>
+                    <td class="text-center">{{header.isMetadata}}</td>
+                    <td class="text-center">{{header.index}}</td>
+                    <td>{{header.type.name}}</td>
+                </tr>
+            </table>
+        </div>
+        <div id="pipeconf-actions">
+            <h3>Actions</h3>
+            <div ng-show="!collapseActions"
+                 class="collapse-btn" icon icon-id="plus" icon-size="30"
+                 ng-click="collapseActions = !collapseActions"></div>
+            <div ng-show="collapseActions"
+                 class="collapse-btn" icon icon-id="minus" icon-size="30"
+                 ng-click="collapseActions = !collapseActions"></div>
+            <table class="pipeconf-table">
+                <tr>
+                    <th style="width: 160px">Name</th>
+                    <th>Action parameters</th>
+                </tr>
+                <tr ng-show="collapseActions">
+                    <td colspan="2" class="clickable text-center"
+                        ng-click="collapseActions = !collapseActions">....</td>
+                </tr>
+                <tr ng-show="!collapseActions && pipelineModel.actions.length === 0">
+                    <td colspan="2" class="no-data">No Data</td>
+                </tr>
+                <tr ng-show="!collapseActions"
+                    ng-repeat="action in pipelineModel.actions"
+                    ng-click="actionSelectCb($event, action)"
+                    ng-class="{selected: action.name === selectedId.name && selectedId.type === 'action'}"
+                    class="clickable">
+                    <td style="width: 160px">{{action.name}}</td>
+                    <td ng-show="action.params.length != 0">{{ mapToNames(action.params).join(', ') }}</td>
+                    <td ng-show="action.params.length == 0">No action params</td>
+                </tr>
+            </table>
+        </div>
+
+        <div id="pipeconf-tables" ng-show-end>
+            <h3>Tables</h3>
+            <div ng-show="!collapseTables" class="collapse-btn"
+                 icon icon-id="plus" icon-size="30"
+                 ng-click="collapseTables = !collapseTables"></div>
+            <div ng-show="collapseTables" class="collapse-btn"
+                 icon icon-id="minus" icon-size="30"
+                 ng-click="collapseTables = !collapseTables"></div>
+            <table class="pipeconf-table">
+                <tr>
+                    <th class="text-center" style="width: 160px">Name</th>
+                    <th class="text-center" style="width: 100px">Max size</th>
+                    <th class="text-center" style="width: 100px">Has Counters</th>
+                    <th class="text-center" style="width: 100px">Support Aging</th>
+                    <th>Match fields</th>
+                    <th>Actions</th>
+                </tr>
+                <tr ng-show="collapseTables">
+                    <td colspan="6" class="clickable text-center"
+                        ng-click="collapseTables = !collapseTables">....</td>
+                </tr>
+                <tr ng-show="!collapseTables && pipelineModel.tables.length === 0">
+                    <td colspan="6" class="no-data">No Data</td>
+                </tr>
+                <tr ng-show="!collapseTables"
+                    ng-repeat="table in pipelineModel.tables"
+                    ng-click="tableSelectCb($event, table)"
+                    ng-class="{selected: table.name === selectedId.name && selectedId.type === 'table'}"
+                    class="clickable">
+                    <td class="text-center">{{table.name}}</td>
+                    <td class="text-center">{{table.maxSize}}</td>
+                    <td class="text-center">{{table.hasCounters}}</td>
+                    <td class="text-center">{{table.supportAging}}</td>
+                    <td ng-show="table.matchFields.length != 0">
+                        {{matMatchFields(table.matchFields).join(', ')}}
+                    </td>
+                    <td ng-show="table.matchFields.length == 0">No match fields</td>
+                    <td ng-show="table.actions.length != 0">{{table.actions.join(", ")}}</td>
+                    <td ng-show="table.actions.length == 0">No table actions</td>
+                </tr>
+            </table>
+        </div>
+        <!-- ng-show-end for checking pipeconf !== null -->
+    </div>
+    <pipeconf-view-detail-panel></pipeconf-view-detail-panel>
+</div>
\ No newline at end of file
diff --git a/web/gui/src/main/webapp/app/view/pipeconf/pipeconf.js b/web/gui/src/main/webapp/app/view/pipeconf/pipeconf.js
new file mode 100644
index 0000000..f5156bb
--- /dev/null
+++ b/web/gui/src/main/webapp/app/view/pipeconf/pipeconf.js
@@ -0,0 +1,464 @@
+/*
+ * Copyright 2017-present Open 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 -- Pipeconf View Module
+ */
+
+(function () {
+    'use strict';
+
+    // injected refs
+    var $log, $scope, $loc, $interval, $timeout, fs, ns, wss, ls, ps, mast, is, dps;
+
+    // Constants
+    var pipeconfRequest = "pipeconfRequest",
+        pipeConfResponse = "pipeConfResponse",
+        noPipeconfResp = "noPipeconfResp",
+        invalidDevId = "invalidDevId",
+        pipeconf = "pipeconf",
+        pipelineModel = "pipelineModel",
+        devId = "devId",
+        topPdg = 28,
+        pName = 'pipeconf-detail-panel',
+        refreshRate = 5000;
+
+    // For request handling
+    var handlers,
+        refreshPromise;
+
+    // Details panel
+    var pWidth = 600,
+        pTopHeight = 111,
+        pStartY,
+        wSize,
+        pHeight,
+        detailsPanel;
+
+    // create key bindings to handle panel
+    var keyBindings = {
+        esc: [closePanel, 'Close the details panel'],
+        _helpFormat: ['esc'],
+    };
+
+    function fetchPipeconfData() {
+        if ($scope.autoRefresh && wss.isConnected() && !ls.waiting()) {
+            ls.start();
+            var requestData = {
+                devId: $scope.devId
+            };
+            wss.sendEvent(pipeconfRequest, requestData);
+        }
+    }
+
+    function pipeConfRespCb(data) {
+        ls.stop();
+        if (!data.hasOwnProperty(pipeconf)) {
+            $scope.pipeconf = null;
+            return;
+        }
+        $scope.pipeconf = data[pipeconf];
+        $scope.pipelineModel = data[pipelineModel];
+        $scope.$apply();
+    }
+
+    function noPipeconfRespCb(data) {
+        ls.stop();
+        $scope.pipeconf = null;
+        $scope.pipelineModel = null;
+        $scope.$apply();
+    }
+
+    function viewDestroy() {
+        wss.unbindHandlers(handlers);
+        $interval.cancel(refreshPromise);
+        refreshPromise = null;
+        ls.stop();
+    }
+
+    function headerSelectCb($event, header) {
+        if ($scope.selectedId !== null &&
+            $scope.selectedId.type === 'header' &&
+            $scope.selectedId.name === header.name) {
+
+            // Hide the panel when select same row
+            closePanel();
+            return;
+        }
+
+        $scope.selectedId = {
+            type: 'header',
+            name: header.name,
+        };
+
+        var subtitles = [
+            {
+                label: 'Header Type: ',
+                value: header.type.name,
+            },
+            {
+                label: 'Is metadata: ',
+                value: header.isMetadata,
+            },
+            {
+                label: 'Index: ',
+                value: header.index,
+            },
+        ];
+
+        var tables = [
+            {
+                title: 'Fields',
+                headers: ['Name', 'Bit width'],
+                data: header.type.fields,
+                noDataText: 'No header fields'
+            },
+        ];
+        populateDetailPanel(header.name, subtitles, tables);
+    }
+
+    function actionSelectCb($event, action) {
+        if ($scope.selectedId !== null &&
+            $scope.selectedId.type === 'action' &&
+            $scope.selectedId.name === action.name) {
+
+            // Hide the panel when select same row
+            closePanel();
+            return;
+        }
+
+        $scope.selectedId = {
+            type: 'action',
+            name: action.name,
+        };
+
+        var subtitles = [];
+        var tables = [
+            {
+                title: 'Parameters',
+                headers: ['Name', 'Bit width'],
+                data: action.params,
+                noDataText: 'No action parameters',
+            },
+        ];
+
+        populateDetailPanel(action.name, subtitles, tables);
+    }
+
+    function tableSelectCb($event, table) {
+        if ($scope.selectedId !== null &&
+            $scope.selectedId.type === 'table' &&
+            $scope.selectedId.name === table.name) {
+
+            // Hide the panel when select same row
+            closePanel();
+            return;
+        }
+
+        $scope.selectedId = {
+            type: 'table',
+            name: table.name,
+        };
+
+        var subtitles = [
+            {
+                label: 'Max Size: ',
+                value: table.maxSize,
+            },
+            {
+                label: 'Has counters: ',
+                value: table.hasCounters,
+            },
+            {
+                label: 'Support Aging: ',
+                value: table.supportAging,
+            },
+        ];
+
+        var matchFields = table.matchFields.map(function(mp) {
+            return {
+                name: mp.header.name + '.' + mp.field.name,
+                bitWidth: mp.field.bitWidth,
+                matchType: mp.matchType,
+            }
+        });
+
+        var tables = [
+            {
+                title: 'Match fields',
+                headers: ['Name', 'Bit width', 'Match type'],
+                data: matchFields,
+                noDataText: 'No match fields'
+            },
+            {
+                title: 'Actions',
+                headers: ['Name'],
+                data: table.actions,
+                noDataText: 'No actions'
+            },
+        ];
+
+        populateDetailPanel(table.name, subtitles, tables);
+    }
+
+    function closePanel() {
+        if (detailsPanel.isVisible()) {
+
+            detailsPanel.hide();
+
+            // Avoid Angular inprog error
+            $timeout(function() {
+                $scope.selectedId = null;
+            }, 0);
+            return true;
+        }
+        return false;
+    }
+
+    function populateDetailTable(tableContainer, table) {
+        var tableTitle = table.title;
+        var tableData = table.data;
+        var tableHeaders = table.headers;
+        var noDataText = table.noDataText;
+
+        tableContainer.append('h2').classed('detail-panel-bottom-title', true).text(tableTitle);
+
+        var detailPanelTable = tableContainer.append('table').classed('detail-panel-table', true);
+        var headerTr = detailPanelTable.append('tr').classed('detail-panel-table-header', true);
+
+        tableHeaders.forEach(function(h) {
+            headerTr.append('th').text(h);
+        });
+
+        if (tableData.length === 0) {
+            var row = detailPanelTable.append('tr').classed('detail-panel-table-row', true);
+            row.append('td')
+                .classed('detail-panel-table-col no-data', true)
+                .attr('colspan', tableHeaders.length)
+                .text(noDataText);
+        }
+
+        tableData.forEach(function(data) {
+            var row = detailPanelTable.append('tr').classed('detail-panel-table-row', true);
+            if (fs.isS(data)) {
+                row.append('td').classed('detail-panel-table-col', true).text(data);
+            } else {
+                Object.keys(data).forEach(function(k) {
+                    row.append('td').classed('detail-panel-table-col', true).text(data[k]);
+                });
+            }
+        });
+
+        tableContainer.append('hr');
+    }
+
+    function populateDetailTables(tableContainer, tables) {
+        tables.forEach(function(table) {
+            populateDetailTable(tableContainer, table);
+        })
+    }
+
+    function populateDetailPanel(topTitle, topSubtitles, tables) {
+        dps.empty();
+        dps.addContainers();
+        dps.addCloseButton(closePanel);
+
+        var top = dps.top();
+        top.append('h2').classed('detail-panel-header', true).text(topTitle);
+        topSubtitles.forEach(function(st) {
+            var typeText = top.append('div').classed('top-info', true);
+            typeText.append('p').classed('label', true).text(st.label);
+            typeText.append('p').classed('value', true).text(st.value);
+        });
+
+        var bottom = dps.bottom();
+        var bottomHeight = pHeight - pTopHeight - 60;
+        bottom.style('height', bottomHeight + 'px');
+        populateDetailTables(bottom, tables);
+
+        detailsPanel.width(pWidth);
+        detailsPanel.show();
+        resizeDetailPanel();
+    }
+
+    function heightCalc() {
+        pStartY = fs.noPxStyle(d3.select('.tabular-header'), 'height')
+            + mast.mastHeight() + topPdg;
+        wSize = fs.windowSize(pStartY);
+        pHeight = wSize.height - 20;
+    }
+
+    function resizeDetailPanel() {
+        if (detailsPanel.isVisible()) {
+            heightCalc();
+            var bottomHeight = pHeight - pTopHeight - 60;
+            d3.select('.bottom').style('height', bottomHeight + 'px');
+            detailsPanel.height(pHeight);
+        }
+    }
+
+    angular.module('ovPipeconf', [])
+        .controller('OvPipeconfCtrl',
+            ['$log', '$scope', '$location', '$interval', '$timeout', 'FnService', 'NavService', 'WebSocketService',
+                'LoadingService', 'PanelService', 'MastService', 'IconService', 'DetailsPanelService',
+                function (_$log_, _$scope_, _$loc_, _$interval_, _$timeout_, _fs_,
+                          _ns_, _wss_, _ls_, _ps_, _mast_, _is_, _dps_) {
+                    $log = _$log_;
+                    $scope = _$scope_;
+                    $loc = _$loc_;
+                    $interval = _$interval_;
+                    $timeout = _$timeout_;
+                    fs = _fs_;
+                    ns = _ns_;
+                    wss = _wss_;
+                    ls = _ls_;
+                    ps = _ps_;
+                    mast = _mast_;
+                    is = _is_;
+                    dps = _dps_;
+
+                    $scope.deviceTip = 'Show device table';
+                    $scope.flowTip = 'Show flow view for this device';
+                    $scope.portTip = 'Show port view for this device';
+                    $scope.groupTip = 'Show group view for this device';
+                    $scope.meterTip = 'Show meter view for selected device';
+                    $scope.pipeconfTip = 'Show pipeconf view for selected device';
+
+                    var params = $loc.search();
+                    if (params.hasOwnProperty(devId)) {
+                        $scope.devId = params[devId];
+                    }
+                    $scope.nav = function (path) {
+                        if ($scope.devId) {
+                            ns.navTo(path, { devId: $scope.devId });
+                        }
+                    };
+                    handlers = {
+                        pipeConfResponse: pipeConfRespCb,
+                        noPipeconfResp: noPipeconfRespCb,
+                        invalidDevId: noPipeconfRespCb,
+                    };
+                    wss.bindHandlers(handlers);
+                    $scope.$on('$destroy', viewDestroy);
+
+                    $scope.autoRefresh = true;
+                    fetchPipeconfData();
+
+                    // On click callbacks, initialize select id
+                    $scope.selectedId = null;
+                    $scope.headerSelectCb = headerSelectCb;
+                    $scope.actionSelectCb = actionSelectCb;
+                    $scope.tableSelectCb = tableSelectCb;
+
+                    // Make them collapsable
+                    $scope.collapsePipeconf = false;
+                    $scope.collapseHeaders = false;
+                    $scope.collapseActions = false;
+                    $scope.collapseTables = false;
+
+                    $scope.mapToNames = function(data) {
+                        return data.map(function(d) {
+                            return d.name;
+                        });
+                    };
+
+                    $scope.matMatchFields = function(matchFields) {
+                        return matchFields.map(function(mf) {
+                            return mf.header.name + '.' + mf.field.name;
+                        });
+                    };
+
+                    refreshPromise = $interval(function() {
+                        fetchPipeconfData();
+                    }, refreshRate);
+
+                    $log.log('OvPipeconfCtrl has been created');
+                }])
+        .directive('autoHeight', ['$window', 'FnService',
+            function($window, fs) {
+                return function(scope, element) {
+                    var autoHeightElem = d3.select(element[0]);
+
+                    scope.$watchCollection(function() {
+                        return {
+                            h: $window.innerHeight
+                        };
+                    }, function() {
+                        var wsz = fs.windowSize(140, 0);
+                        autoHeightElem.style('height', wsz.height + 'px');
+                    });
+                };
+            }
+        ])
+        .directive('pipeconfViewDetailPanel', ['$rootScope', '$window', '$timeout', 'KeyService',
+            function($rootScope, $window, $timeout, ks) {
+                function createDetailsPanel() {
+                    detailsPanel = dps.create(pName, {
+                        width: wSize.width,
+                        margin: 0,
+                        hideMargin: 0,
+                        scope: $scope,
+                        keyBindings: keyBindings,
+                        nameChangeRequest: null,
+                    });
+                    $scope.hidePanel = function () { detailsPanel.hide(); };
+                    detailsPanel.hide();
+                }
+
+                function initPanel() {
+                    heightCalc();
+                    createDetailsPanel();
+                }
+
+                return function(scope) {
+                    var unbindWatch;
+                    // Safari has a bug where it renders the fixed-layout table wrong
+                    // if you ask for the window's size too early
+                    if (scope.onos.browser === 'safari') {
+                        $timeout(initPanel);
+                    } else {
+                        initPanel();
+                    }
+
+                    // if the panelData changes
+                    scope.$watch('panelData', function () {
+                        if (!fs.isEmptyObject(scope.panelData)) {
+                            // populateDetails(scope.panelData);
+                            detailsPanel.show();
+                        }
+                    });
+
+                    // if the window size changes
+                    unbindWatch = $rootScope.$watchCollection(
+                        function () {
+                            return {
+                                h: $window.innerHeight,
+                                w: $window.innerWidth,
+                            };
+                        }, function () {
+                            resizeDetailPanel();
+                        }
+                    );
+
+                    scope.$on('$destroy', function () {
+                        unbindWatch();
+                        ks.unbindKeys();
+                        ps.destroyPanel(pName);
+                    });
+                };
+            }
+        ]);
+}());
diff --git a/web/gui/src/main/webapp/app/view/port/port.html b/web/gui/src/main/webapp/app/view/port/port.html
index 753ca22..f6e9b2e 100644
--- a/web/gui/src/main/webapp/app/view/port/port.html
+++ b/web/gui/src/main/webapp/app/view/port/port.html
@@ -48,6 +48,11 @@
                  icon icon-id="meterTable" icon-size="42"
                  tooltip tt-msg="meterTip"
                  ng-click="nav('meter')"></div>
+
+            <div class="active"
+                 icon icon-id="pipeconfTable" icon-size="42"
+                 tooltip tt-msg="pipeconfTip"
+                 ng-click="nav('pipeconf')"></div>
         </div>
 
         <div class="search">
diff --git a/web/gui/src/main/webapp/app/view/port/port.js b/web/gui/src/main/webapp/app/view/port/port.js
index 7aad374..5bf7871 100644
--- a/web/gui/src/main/webapp/app/view/port/port.js
+++ b/web/gui/src/main/webapp/app/view/port/port.js
@@ -90,6 +90,7 @@
                 $scope.flowTip = 'Show flow view for this device';
                 $scope.groupTip = 'Show group view for this device';
                 $scope.meterTip = 'Show meter view for selected device';
+                $scope.pipeconfTip = 'Show pipeconf view for selected device';
                 $scope.toggleDeltaTip = 'Toggle port delta statistics';
                 $scope.toggleNZTip = 'Toggle non zero port statistics';