This is an automated email from the ASF dual-hosted git repository.

aichrist pushed a commit to branch analytics-framework
in repository https://gitbox.apache.org/repos/asf/nifi.git

commit b74c4dfb8434650aebe350ef90a9e041cbe1980b
Author: Rob Fellows <[email protected]>
AuthorDate: Mon Jul 22 17:02:48 2019 -0400

    NIFI-6568 - Surface time-to-back-pressure and initial predictions in the UI
    * Add multi-line tooltips with detail for connection queue back pressure 
graphics.
    * Add estimated time to back pressure to connections summary table.
    * Add back pressure prediction ticks.
    * add moment.js to format predicted time to back pressure
    * tweak summary table headings to match data displayed. re-order connection 
summary columns
---
 nifi-assembly/LICENSE                              |  25 ++
 .../src/main/resources/META-INF/LICENSE            |  25 ++
 .../nifi-framework/nifi-web/nifi-web-ui/pom.xml    |   4 +
 .../src/main/frontend/package-lock.json            |  21 +-
 .../nifi-web-ui/src/main/frontend/package.json     |   1 +
 .../src/main/resources/META-INF/LICENSE            |  25 ++
 .../main/webapp/WEB-INF/pages/bulletin-board.jsp   |   1 +
 .../src/main/webapp/WEB-INF/pages/canvas.jsp       |   2 +
 .../src/main/webapp/WEB-INF/pages/cluster.jsp      |   1 +
 .../src/main/webapp/WEB-INF/pages/counters.jsp     |   1 +
 .../src/main/webapp/WEB-INF/pages/history.jsp      |   1 +
 .../src/main/webapp/WEB-INF/pages/login.jsp        |   1 +
 .../src/main/webapp/WEB-INF/pages/provenance.jsp   |   1 +
 .../src/main/webapp/WEB-INF/pages/summary.jsp      |   1 +
 .../src/main/webapp/WEB-INF/pages/templates.jsp    |   1 +
 .../src/main/webapp/WEB-INF/pages/users.jsp        |   1 +
 .../nifi-web-ui/src/main/webapp/css/graph.css      |  25 +-
 .../src/main/webapp/js/nf/canvas/nf-connection.js  | 333 +++++++++++++++++----
 .../nifi-web-ui/src/main/webapp/js/nf/nf-common.js |  22 +-
 .../main/webapp/js/nf/summary/nf-summary-table.js  | 140 ++++++---
 20 files changed, 525 insertions(+), 107 deletions(-)

diff --git a/nifi-assembly/LICENSE b/nifi-assembly/LICENSE
index 123125b..eb00732 100644
--- a/nifi-assembly/LICENSE
+++ b/nifi-assembly/LICENSE
@@ -2583,6 +2583,31 @@ This product bundles 'lodash' which is available under 
an MIT license.
     licenses; we recommend you read them, as their terms may differ from the
     terms above.
 
+This product bundles 'moment' which is available under an MIT license.
+
+    Copyright (c) JS Foundation and other contributors
+
+    Permission is hereby granted, free of charge, to any person
+    obtaining a copy of this software and associated documentation
+    files (the "Software"), to deal in the Software without
+    restriction, including without limitation the rights to use,
+    copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the
+    Software is furnished to do so, subject to the following
+    conditions:
+
+    The above copyright notice and this permission notice shall be
+    included in all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+    EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+    OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+    NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+    HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+    WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+    OTHER DEALINGS IN THE SOFTWARE.
+
 The binary distribution of this product bundles 'normalize.css'
 
   NORMALIZE.CSS LICENSE
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework-nar/src/main/resources/META-INF/LICENSE
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework-nar/src/main/resources/META-INF/LICENSE
index 679589f..c6530dd 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework-nar/src/main/resources/META-INF/LICENSE
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework-nar/src/main/resources/META-INF/LICENSE
@@ -853,3 +853,28 @@ This product bundles 'lodash' which is available under an 
MIT license.
     maintained libraries used by this software which have their own
     licenses; we recommend you read them, as their terms may differ from the
     terms above.
+
+This product bundles 'moment' which is available under an MIT license.
+
+    Copyright (c) JS Foundation and other contributors
+
+    Permission is hereby granted, free of charge, to any person
+    obtaining a copy of this software and associated documentation
+    files (the "Software"), to deal in the Software without
+    restriction, including without limitation the rights to use,
+    copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the
+    Software is furnished to do so, subject to the following
+    conditions:
+
+    The above copyright notice and this permission notice shall be
+    included in all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+    EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+    OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+    NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+    HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+    WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+    OTHER DEALINGS IN THE SOFTWARE.
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/pom.xml
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/pom.xml
index ef283de..08dca29 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/pom.xml
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/pom.xml
@@ -239,6 +239,10 @@
                                         
<include>lodash-core/distrib/lodash-core.min.js</include>
                                         
<include>lodash-core/distrib/README.md</include>
 
+                                        
<include>moment/min/moment.min.js</include>
+                                        <include>moment/README.md</include>
+                                        <include>moment/LICENSE</include>
+
                                     </includes>
                                 </resource>
                             </resources>
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/frontend/package-lock.json
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/frontend/package-lock.json
index 8238e32..9836aa9 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/frontend/package-lock.json
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/frontend/package-lock.json
@@ -88,9 +88,9 @@
       }
     },
     "commander": {
-      "version": "2.16.0",
-      "resolved": 
"https://registry.npmjs.org/commander/-/commander-2.16.0.tgz";,
-      "integrity": 
"sha512-sVXqklSaotK9at437sFlFpyOcJonxe0yST/AG9DkQKUdIE6IqGIMv4SfAQSKaJbSdVEJYItASCrBiVQHq1HQew=="
+      "version": "2.20.0",
+      "resolved": 
"https://registry.npmjs.org/commander/-/commander-2.20.0.tgz";,
+      "integrity": 
"sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ=="
     },
     "d3": {
       "version": "4.13.0",
@@ -189,8 +189,8 @@
       "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-1.0.8.tgz";,
       "integrity": 
"sha512-IVCJpQ+YGe3qu6odkPQI0KPqfxkhbP/oM1XhhE/DFiYmcXKfCRub4KXyiuehV1d4drjWVXHUWx4gHqhdZb6n/A==",
       "requires": {
-        "commander": "2.16.0",
-        "iconv-lite": "0.4.23",
+        "commander": "2.20.0",
+        "iconv-lite": "0.4.24",
         "rw": "1.3.3"
       }
     },
@@ -372,9 +372,9 @@
       "integrity": "sha1-ZxRKUmDDT8PMpnfQQdr1L+e3iy8="
     },
     "iconv-lite": {
-      "version": "0.4.23",
-      "resolved": 
"https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz";,
-      "integrity": 
"sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==",
+      "version": "0.4.24",
+      "resolved": 
"https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz";,
+      "integrity": 
"sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
       "requires": {
         "safer-buffer": "2.1.2"
       }
@@ -432,6 +432,11 @@
       "resolved": 
"https://registry.npmjs.org/lodash-core/-/lodash-core-4.17.11.tgz";,
       "integrity": 
"sha512-8ilprNE67U1REh0wQHL0z37qXdsxuFXjvxehg79Mh/MQgNJ+C1muXtysSKpt9sCxXZUSiwifEC82Vtzf2GSSKQ=="
     },
+    "moment": {
+      "version": "2.24.0",
+      "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz";,
+      "integrity": 
"sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg=="
+    },
     "nomnom": {
       "version": "1.8.1",
       "resolved": "https://registry.npmjs.org/nomnom/-/nomnom-1.8.1.tgz";,
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/frontend/package.json
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/frontend/package.json
index b45e1a1..2f0fb75 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/frontend/package.json
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/frontend/package.json
@@ -35,6 +35,7 @@
     "reset.css": "2.0.2",
     "jquery-form": "3.50.0",
     "lodash-core": "4.17.11",
+    "moment": "2.24.0",
     "url-search-params": "0.6.1",
     "jsonlint": "1.6.2",
     "qtip2": "3.0.3",
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/META-INF/LICENSE
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/META-INF/LICENSE
index 9e1b605..fc0572d 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/META-INF/LICENSE
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/META-INF/LICENSE
@@ -771,3 +771,28 @@ This product bundles 'lodash' which is available under an 
MIT license.
     maintained libraries used by this software which have their own
     licenses; we recommend you read them, as their terms may differ from the
     terms above.
+
+This product bundles 'moment' which is available under an MIT license.
+
+    Copyright (c) JS Foundation and other contributors
+
+    Permission is hereby granted, free of charge, to any person
+    obtaining a copy of this software and associated documentation
+    files (the "Software"), to deal in the Software without
+    restriction, including without limitation the rights to use,
+    copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the
+    Software is furnished to do so, subject to the following
+    conditions:
+
+    The above copyright notice and this permission notice shall be
+    included in all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+    EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+    OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+    NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+    HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+    WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+    OTHER DEALINGS IN THE SOFTWARE.
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/bulletin-board.jsp
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/bulletin-board.jsp
index 822e361..879dcb2 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/bulletin-board.jsp
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/bulletin-board.jsp
@@ -39,6 +39,7 @@
         <script type="text/javascript" src="js/jquery/jquery.each.js"></script>
         <script type="text/javascript" 
src="assets/jquery-ui-dist/jquery-ui.min.js"></script>
         <script type="text/javascript" 
src="assets/lodash-core/distrib/lodash-core.min.js"></script>
+        <script type="text/javascript" 
src="assets/moment/min/moment.min.js"></script>
         <script type="text/javascript" 
src="assets/qtip2/dist/jquery.qtip.min.js"></script>
         <script type="text/javascript" 
src="assets/angular/angular.min.js"></script>
         <script type="text/javascript" 
src="assets/angular-messages/angular-messages.min.js"></script>
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/canvas.jsp
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/canvas.jsp
index cee2c5e..4fde541 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/canvas.jsp
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/canvas.jsp
@@ -62,6 +62,7 @@
         <script type="text/javascript" 
src="js/jquery/modal/jquery.modal.js?${project.version}"></script>
         <script type="text/javascript" 
src="assets/jquery-minicolors/jquery.minicolors.min.js"></script>
         <script type="text/javascript" 
src="assets/lodash-core/distrib/lodash-core.min.js"></script>
+        <script type="text/javascript" 
src="assets/moment/min/moment.min.js"></script>
         <script type="text/javascript" 
src="assets/qtip2/dist/jquery.qtip.min.js"></script>
         <script type="text/javascript" 
src="assets/slickgrid/lib/jquery.event.drag-2.3.0.js"></script>
         <script type="text/javascript" 
src="assets/slickgrid/plugins/slick.cellrangeselector.js"></script>
@@ -130,6 +131,7 @@
             <div id="port-tooltips"></div>
             <div id="process-group-tooltips"></div>
             <div id="remote-process-group-tooltips"></div>
+            <div id="connection-tooltips"></div>
         </div>
         <jsp:include page="/WEB-INF/partials/canvas/navigation.jsp"/>
         <jsp:include page="/WEB-INF/partials/canvas/settings-content.jsp"/>
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/cluster.jsp
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/cluster.jsp
index 61aa5ca..eb8b0da 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/cluster.jsp
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/cluster.jsp
@@ -43,6 +43,7 @@
         <script type="text/javascript" src="js/jquery/jquery.each.js"></script>
         <script type="text/javascript" 
src="assets/jquery-ui-dist/jquery-ui.min.js"></script>
         <script type="text/javascript" 
src="assets/lodash-core/distrib/lodash-core.min.js"></script>
+        <script type="text/javascript" 
src="assets/moment/min/moment.min.js"></script>
         <script type="text/javascript" 
src="assets/qtip2/dist/jquery.qtip.min.js"></script>
         <script type="text/javascript" 
src="assets/slickgrid/lib/jquery.event.drag-2.3.0.js"></script>
         <script type="text/javascript" 
src="assets/slickgrid/plugins/slick.cellrangeselector.js"></script>
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/counters.jsp
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/counters.jsp
index 8c276e3..239f6a6 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/counters.jsp
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/counters.jsp
@@ -41,6 +41,7 @@
         <script type="text/javascript" src="js/jquery/jquery.each.js"></script>
         <script type="text/javascript" 
src="assets/jquery-ui-dist/jquery-ui.min.js"></script>
         <script type="text/javascript" 
src="assets/lodash-core/distrib/lodash-core.min.js"></script>
+        <script type="text/javascript" 
src="assets/moment/min/moment.min.js"></script>
         <script type="text/javascript" 
src="assets/qtip2/dist/jquery.qtip.min.js"></script>
         <script type="text/javascript" 
src="assets/slickgrid/lib/jquery.event.drag-2.3.0.js"></script>
         <script type="text/javascript" 
src="assets/slickgrid/plugins/slick.cellrangeselector.js"></script>
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/history.jsp
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/history.jsp
index 429eaf3..47700df 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/history.jsp
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/history.jsp
@@ -41,6 +41,7 @@
         <script type="text/javascript" src="js/jquery/jquery.each.js"></script>
         <script type="text/javascript" 
src="assets/jquery-ui-dist/jquery-ui.min.js"></script>
         <script type="text/javascript" 
src="assets/lodash-core/distrib/lodash-core.min.js"></script>
+        <script type="text/javascript" 
src="assets/moment/min/moment.min.js"></script>
         <script type="text/javascript" 
src="assets/qtip2/dist/jquery.qtip.min.js"></script>
         <script type="text/javascript" 
src="assets/slickgrid/lib/jquery.event.drag-2.3.0.js"></script>
         <script type="text/javascript" 
src="assets/slickgrid/plugins/slick.cellrangeselector.js"></script>
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/login.jsp
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/login.jsp
index 6efffdc..1204e19 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/login.jsp
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/login.jsp
@@ -40,6 +40,7 @@
         <script type="text/javascript" 
src="assets/jquery-ui-dist/jquery-ui.min.js"></script>
         <script type="text/javascript" 
src="js/nf/nf-namespace.js?${project.version}"></script>
         <script type="text/javascript" 
src="assets/lodash-core/distrib/lodash-core.min.js"></script>
+        <script type="text/javascript" 
src="assets/moment/min/moment.min.js"></script>
         ${nf.login.script.tags}
     </head>
     <body class="login-body">
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/provenance.jsp
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/provenance.jsp
index 18fa0c9..9862a59 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/provenance.jsp
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/provenance.jsp
@@ -45,6 +45,7 @@
         <script type="text/javascript" src="js/jquery/jquery.each.js"></script>
         <script type="text/javascript" 
src="assets/jquery-ui-dist/jquery-ui.min.js"></script>
         <script type="text/javascript" 
src="assets/lodash-core/distrib/lodash-core.min.js"></script>
+        <script type="text/javascript" 
src="assets/moment/min/moment.min.js"></script>
         <script type="text/javascript" 
src="assets/qtip2/dist/jquery.qtip.min.js"></script>
         <script type="text/javascript" 
src="assets/slickgrid/lib/jquery.event.drag-2.3.0.js"></script>
         <script type="text/javascript" 
src="assets/slickgrid/plugins/slick.cellrangeselector.js"></script>
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/summary.jsp
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/summary.jsp
index 394f630..5d6ad09 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/summary.jsp
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/summary.jsp
@@ -50,6 +50,7 @@
         <script type="text/javascript" 
src="js/jquery/jquery.ellipsis.js"></script>
         <script type="text/javascript" src="js/jquery/jquery.each.js"></script>
         <script type="text/javascript" 
src="assets/lodash-core/distrib/lodash-core.min.js"></script>
+        <script type="text/javascript" 
src="assets/moment/min/moment.min.js"></script>
         <script type="text/javascript" 
src="assets/qtip2/dist/jquery.qtip.min.js"></script>
         <script type="text/javascript" 
src="assets/slickgrid/lib/jquery.event.drag-2.3.0.js"></script>
         <script type="text/javascript" 
src="assets/slickgrid/plugins/slick.cellrangeselector.js"></script>
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/templates.jsp
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/templates.jsp
index f176d03..68b95d9 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/templates.jsp
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/templates.jsp
@@ -41,6 +41,7 @@
         <script type="text/javascript" src="js/jquery/jquery.each.js"></script>
         <script type="text/javascript" 
src="assets/jquery-ui-dist/jquery-ui.min.js"></script>
         <script type="text/javascript" 
src="assets/lodash-core/distrib/lodash-core.min.js"></script>
+        <script type="text/javascript" 
src="assets/moment/min/moment.min.js"></script>
         <script type="text/javascript" 
src="assets/qtip2/dist/jquery.qtip.min.js"></script>
         <script type="text/javascript" 
src="assets/slickgrid/lib/jquery.event.drag-2.3.0.js"></script>
         <script type="text/javascript" 
src="assets/slickgrid/plugins/slick.cellrangeselector.js"></script>
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/users.jsp
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/users.jsp
index 667f2ef..4295fdf 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/users.jsp
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/users.jsp
@@ -43,6 +43,7 @@
         <script type="text/javascript" 
src="js/jquery/jquery.ellipsis.js"></script>
         <script type="text/javascript" src="js/jquery/jquery.each.js"></script>
         <script type="text/javascript" 
src="assets/lodash-core/distrib/lodash-core.min.js"></script>
+        <script type="text/javascript" 
src="assets/moment/min/moment.min.js"></script>
         <script type="text/javascript" 
src="assets/qtip2/dist/jquery.qtip.min.js"></script>
         <script type="text/javascript" 
src="assets/slickgrid/lib/jquery.event.drag-2.3.0.js"></script>
         <script type="text/javascript" 
src="assets/slickgrid/plugins/slick.cellrangeselector.js"></script>
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/graph.css
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/graph.css
index 4c9f289..f47771b 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/graph.css
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/graph.css
@@ -344,7 +344,24 @@ text.connection-from-run-status.is-missing-port, 
text.connection-to-run-status.i
 }
 
 g.connection rect.backpressure-tick {
-    fill: #3e3e3e;
+    fill: transparent;
+}
+
+g.connection rect.backpressure-tick.data-size-prediction.prediction-down,
+g.connection rect.backpressure-tick.object-prediction.prediction-down {
+    fill: white;
+}
+
+g.connection rect.backpressure-tick.data-size-prediction,
+g.connection rect.backpressure-tick.object-prediction {
+    fill: black;
+}
+
+g.connection rect.backpressure-tick.data-size-prediction.not-configured,
+g.connection rect.backpressure-tick.object-prediction.not-configured,
+g.connection 
rect.backpressure-tick.data-size-prediction.prediction-down.not-configured,
+g.connection 
rect.backpressure-tick.object-prediction.prediction-down.not-configured {
+    fill: transparent;
 }
 
 g.connection rect.backpressure-tick.not-configured {
@@ -355,6 +372,12 @@ g.connection rect.backpressure-object, g.connection 
rect.backpressure-data-size
     fill: #d8d8d8;
 }
 
+/**/
+g.connection rect.backpressure-object>title>tspan, g.connection 
rect.backpressure-data-size>title>tspan {
+    display: block;
+}
+/**/
+
 g.connection rect.backpressure-object.not-configured, g.connection 
rect.backpressure-data-size.not-configured {
     fill: transparent;
 }
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-connection.js
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-connection.js
index b325c32..2a50f4c 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-connection.js
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-connection.js
@@ -26,9 +26,10 @@
                 'nf.Storage',
                 'nf.ErrorHandler',
                 'nf.Client',
-                'nf.CanvasUtils'],
-            function ($, d3, nfCommon, nfDialog, nfStorage, nfErrorHandler, 
nfClient, nfCanvasUtils) {
-                return (nf.Connection = factory($, d3, nfCommon, nfDialog, 
nfStorage, nfErrorHandler, nfClient, nfCanvasUtils));
+                'nf.CanvasUtils',
+                'lodash-core'],
+            function ($, d3, nfCommon, nfDialog, nfStorage, nfErrorHandler, 
nfClient, nfCanvasUtils, _) {
+                return (nf.Connection = factory($, d3, nfCommon, nfDialog, 
nfStorage, nfErrorHandler, nfClient, nfCanvasUtils, _));
             });
     } else if (typeof exports === 'object' && typeof module === 'object') {
         module.exports = (nf.Connection =
@@ -39,7 +40,8 @@
                 require('nf.Storage'),
                 require('nf.ErrorHandler'),
                 require('nf.Client'),
-                require('nf.CanvasUtils')));
+                require('nf.CanvasUtils'),
+                require('lodash-code')));
     } else {
         nf.Connection = factory(root.$,
             root.d3,
@@ -48,9 +50,10 @@
             root.nf.Storage,
             root.nf.ErrorHandler,
             root.nf.Client,
-            root.nf.CanvasUtils);
+            root.nf.CanvasUtils,
+            root._);
     }
-}(this, function ($, d3, nfCommon, nfDialog, nfStorage, nfErrorHandler, 
nfClient, nfCanvasUtils) {
+}(this, function ($, d3, nfCommon, nfDialog, nfStorage, nfErrorHandler, 
nfClient, nfCanvasUtils, _) {
     'use strict';
 
     var nfSelectable;
@@ -66,6 +69,9 @@
     // width of a backpressure indicator - half of width, left/right padding, 
left/right border
     var backpressureBarWidth = (dimensions.width / 2) - 15 - 2;
 
+    var backpressureCountOffset = 6;
+    var backpressureDataSizeOffset = (dimensions.width / 2) + 10 + 1;
+
     // --------------------------
     // Snap alignment for drag events
     // --------------------------
@@ -1238,92 +1244,121 @@
                         var yBackpressureOffset = rowHeight + 
HEIGHT_FOR_BACKPRESSURE - 4;
 
                         // backpressure object threshold
+                        var backpressureObjectContainer = queued.append('g')
+                            .attrs({
+                                'transform': 'translate(' + 
backpressureCountOffset + ', ' + yBackpressureOffset + ')',
+                                'class': 'backpressure-object-container'
+                            });
 
                         // start
-                        queued.append('rect')
+                        backpressureObjectContainer.append('rect')
                             .attrs({
                                 'class': 'backpressure-tick object',
                                 'width': 1,
                                 'height': 3,
-                                'x': 5,
-                                'y': yBackpressureOffset
+                                'x': 0,
+                                'y': 0
                             });
 
                         // bar
-                        var backpressureCountOffset = 6;
-                        queued.append('rect')
+                        backpressureObjectContainer.append('rect')
                             .attrs({
                                 'class': 'backpressure-object',
                                 'width': backpressureBarWidth,
                                 'height': 3,
-                                'x': backpressureCountOffset,
-                                'y': yBackpressureOffset
-                            })
-                            .append('title');
+                                'x': 0,
+                                'y': 0
+                            });
 
                         // end
-                        queued.append('rect')
+                        backpressureObjectContainer.append('rect')
                             .attrs({
                                 'class': 'backpressure-tick object',
                                 'width': 1,
                                 'height': 3,
-                                'x': backpressureCountOffset + 
backpressureBarWidth,
-                                'y': yBackpressureOffset
+                                'x': backpressureBarWidth,
+                                'y': 0
                             });
 
                         // percent full
-                        queued.append('rect')
+                        backpressureObjectContainer.append('rect')
                             .attrs({
                                 'class': 'backpressure-percent object',
                                 'width': 0,
                                 'height': 3,
-                                'x': backpressureCountOffset,
-                                'y': yBackpressureOffset
+                                'x': 0,
+                                'y': 0
+                            });
+
+                        // prediction indicator
+                        backpressureObjectContainer.append('rect')
+                            .attrs({
+                                'class': 'backpressure-tick object-prediction',
+                                'width': 1,
+                                'height': 3,
+                                'x': backpressureBarWidth,
+                                'y': 0
                             });
 
                         // backpressure data size threshold
 
+                        var backpressureDataSizeContainer = queued.append('g')
+                            .attrs({
+                                'transform': 'translate(' + 
backpressureDataSizeOffset + ', ' + yBackpressureOffset + ')',
+                                'class': 'backpressure-data-size-container'
+                            });
+
                         // start
-                        queued.append('rect')
+                        backpressureDataSizeContainer.append('rect')
                             .attrs({
                                 'class': 'backpressure-tick data-size',
                                 'width': 1,
                                 'height': 3,
-                                'x': (dimensions.width / 2) + 10,
-                                'y': yBackpressureOffset
+                                'x': 0,
+                                'y': 0
                             });
 
                         // bar
-                        var backpressureDataSizeOffset = (dimensions.width / 
2) + 10 + 1;
-                        queued.append('rect')
+                        backpressureDataSizeContainer.append('rect')
                             .attrs({
                                 'class': 'backpressure-data-size',
                                 'width': backpressureBarWidth,
                                 'height': 3,
-                                'x': backpressureDataSizeOffset,
-                                'y': yBackpressureOffset
+                                'x': 0,
+                                'y': 0
                             })
                             .append('title');
 
                         // end
-                        queued.append('rect')
+                        backpressureDataSizeContainer.append('rect')
                             .attrs({
                                 'class': 'backpressure-tick data-size',
                                 'width': 1,
                                 'height': 3,
-                                'x': backpressureDataSizeOffset + 
backpressureBarWidth,
-                                'y': yBackpressureOffset
+                                'x': backpressureBarWidth,
+                                'y': 0
                             });
 
                         // percent full
-                        queued.append('rect')
+                        backpressureDataSizeContainer.append('rect')
                             .attrs({
                                 'class': 'backpressure-percent data-size',
                                 'width': 0,
                                 'height': 3,
-                                'x': backpressureDataSizeOffset,
-                                'y': yBackpressureOffset
+                                'x': 0,
+                                'y': 0
                             });
+
+                        // prediction indicator
+                        backpressureDataSizeContainer.append('rect')
+                            .attrs({
+                                'class': 'backpressure-tick 
data-size-prediction',
+                                'width': 1,
+                                'height': 3,
+                                'x': backpressureBarWidth,
+                                'y': 0
+                            });
+
                     } else {
                         
backgrounds.push(queued.select('rect.connection-label-background'));
                         
borders.push(queued.select('rect.connection-label-border'));
@@ -1441,6 +1476,10 @@
                         .classed('not-configured', function () {
                             return 
nfCommon.isUndefinedOrNull(d.status.aggregateSnapshot.percentUseCount);
                         });
+                    
connectionLabelContainer.selectAll('rect.backpressure-tick.object-prediction')
+                        .classed('not-configured', function () {
+                            return 
nfCommon.isUndefinedOrNull(d.status.aggregateSnapshot.percentUseCount);
+                        });
 
                     // update backpressure data size fill
                     
connectionLabelContainer.select('rect.backpressure-data-size')
@@ -1451,6 +1490,10 @@
                         .classed('not-configured', function () {
                             return 
nfCommon.isUndefinedOrNull(d.status.aggregateSnapshot.percentUseBytes);
                         });
+                    
connectionLabelContainer.selectAll('rect.backpressure-tick.data-size-prediction')
+                        .classed('not-configured', function () {
+                            return 
nfCommon.isUndefinedOrNull(d.status.aggregateSnapshot.percentUseBytes);
+                        });
 
                     if (d.permissions.canWrite) {
                         // only support dragging the label when appropriate
@@ -1476,6 +1519,95 @@
         });
     };
 
+    var isAtBackPressure = function (d) {
+        var percentUseCount = _.get(d, 
'status.aggregateSnapshot.percentUseCount', 0);
+        var percentUseBytes = _.get(d, 
'status.aggregateSnapshot.percentUseBytes', 0);
+        return Math.max(percentUseCount, percentUseBytes) >= 100;
+    };
+
+    /**
+     * Gets the tooltip content for the back pressure count metric
+     * @param d
+     */
+    var getBackPressureCountTip = function (d) {
+        var tooltipContent;
+        var percentUseCount = _.get(d, 
'status.aggregateSnapshot.percentUseCount');
+        if (_.isNumber(percentUseCount)) {
+            var objectThreshold = _.get(d, 
'component.backPressureObjectThreshold');
+
+            var predictedPercentCount = _.get(d, 
'status.aggregateSnapshot.predictedPercentCount', -1);
+            var timeToBackPressure = _.get(d, 
'status.aggregateSnapshot.predictedMillisUntilCountBackpressure', -1);
+
+            var tooltipLines = ['Queue: ' + _.clamp(percentUseCount, 0, 100) + 
'% full (based on ' + objectThreshold + ' object threshold)'];
+
+            // only show predicted percent if it is non-negative
+            if (_.isNumber(predictedPercentCount) && predictedPercentCount > 
-1) {
+                var predictionIntervalSeconds = _.get(d, 
'status.aggregateSnapshot.predictionIntervalSeconds', 60 * 5);
+                tooltipLines.push('Predicted queue (next ' + 
(predictionIntervalSeconds / 60 ) + ' mins): ' + _.clamp(predictedPercentCount, 
0, 100) + '%')
+            }
+
+            // only show an estimate if it is valid (non-negative but less 
than the max number supported)
+            if (_.isNumber(timeToBackPressure) && 
_.inRange(timeToBackPressure, 0, Number.MAX_SAFE_INTEGER) && 
!isAtBackPressure(d)) {
+                var duration = 
nfCommon.formatPredictedDuration(timeToBackPressure);
+                tooltipLines.push('Estimated time to back pressure: ' + 
duration);
+            }
+
+            if (_.isEmpty(tooltipLines)) {
+                return '';
+            } else if (_.size(tooltipLines) === 1) {
+                return tooltipLines[0];
+            } else {
+                tooltipContent = nfCommon.formatUnorderedList(tooltipLines)
+            }
+        } else {
+            tooltipContent = 'Back Pressure Object Threshold is not 
configured';
+        }
+
+        return tooltipContent;
+    };
+
+    /**
+     * Gets the tooltip content for the back pressure size metric
+     * @param d
+     */
+    var getBackPressureSizeTip = function (d) {
+        var tooltipContent;
+        var percentUseBytes = _.get(d, 
'status.aggregateSnapshot.percentUseBytes');
+
+        if (_.isNumber(percentUseBytes)) {
+            var dataSizeThreshold = _.get(d, 
'component.backPressureDataSizeThreshold');
+
+            var predictedPercentBytes = _.get(d, 
'status.aggregateSnapshot.predictedPercentBytes', -1);
+            var timeToBackPressure = _.get(d, 
'status.aggregateSnapshot.predictedMillisUntilBytesBackpressure', -1);
+
+            var tooltipLines = ['Queue: ' + _.clamp(percentUseBytes, 0, 100) + 
'% full (based on ' + dataSizeThreshold + ' data size threshold)'];
+
+            // only show predicted percent if it is non-negative
+            if (_.isNumber(predictedPercentBytes) && predictedPercentBytes > 
-1) {
+                var predictionIntervalSeconds = _.get(d, 
'status.aggregateSnapshot.predictionIntervalSeconds', 60 * 5);
+                tooltipLines.push('Predicted queue (next ' + 
(predictionIntervalSeconds / 60 ) + ' mins): ' + _.clamp(predictedPercentBytes, 
0, 100) + '%')
+            }
+
+            // only show an estimate if it is valid (non-negative but less 
than the max number supported)
+            if (_.isNumber(timeToBackPressure) && 
_.inRange(timeToBackPressure, 0, Number.MAX_SAFE_INTEGER) && 
!isAtBackPressure(d)) {
+                var duration = 
nfCommon.formatPredictedDuration(timeToBackPressure);
+                tooltipLines.push('Estimated time to back pressure: ' + 
duration);
+            }
+
+            if (_.isEmpty(tooltipLines)) {
+                return '';
+            } else if (_.size(tooltipLines) === 1) {
+                return tooltipLines[0];
+            } else {
+                tooltipContent = nfCommon.formatUnorderedList(tooltipLines)
+            }
+        } else {
+            tooltipContent = 'Back Pressure Data Size Threshold is not 
configured';
+        }
+
+        return tooltipContent;
+    };
+
     /**
      * Updates the stats of the connections in the specified selection.
      *
@@ -1517,13 +1649,53 @@
                 deferred.resolve();
             });
 
-            
updated.select('rect.backpressure-data-size').select('title').text(function (d) 
{
-                if 
(nfCommon.isDefinedAndNotNull(d.status.aggregateSnapshot.percentUseBytes)) {
-                    return 'Queue is ' + 
d.status.aggregateSnapshot.percentUseBytes + '% full based on Back Pressure 
Data Size Threshold';
-                } else {
-                    return 'Back Pressure Data Size Threshold is not 
configured';
-                }
+            var backpressurePercentDataSizePrediction = 
updated.select('rect.backpressure-tick.data-size-prediction');
+            backpressurePercentDataSizePrediction.transition()
+                .duration(400)
+                .attrs({
+                    'x': function (d) {
+                        // clamp the prediction between 0 and 100 percent
+                        var predicted = _.get(d, 
'status.aggregateSnapshot.predictedPercentBytes', 0);
+                        return (backpressureBarWidth * _.clamp(predicted, 0, 
100)) / 100;
+                    },
+                    'display': function (d) {
+                        var predicted = _.get(d, 
'status.aggregateSnapshot.predictedPercentBytes', -1);
+                        if (predicted >= 0) {
+                            return 'unset';
+                        } else {
+                            // don't show it if there not a valid prediction
+                            return 'none';
+                        }
+                    }
+                }).on('end', function () {
+                    
backpressurePercentDataSizePrediction.classed('prediction-down', function (d) {
+                        var actual = _.get(d, 
'status.aggregateSnapshot.percentUseBytes', 0);
+                        var predicted = _.get(d, 
'status.aggregateSnapshot.predictedPercentBytes', 0);
+                        return predicted < actual;
+                })
             });
+
+            updated.select('g.backpressure-data-size-container')
+                .each(function(d) {
+                    var tip = d3.select('#back-pressure-size-tip-' + d.id);
+
+                    // create a DOM element for the tooltip if ones does not 
already exist
+                    if (tip.empty()) {
+                        tip = d3.select('#connection-tooltips')
+                            .append('div')
+                            .attr('id', function() {
+                                return 'back-pressure-size-tip-' + d.id
+                            })
+                            .attr('class', 'tooltip nifi-tooltip');
+                    }
+
+                    // update the tooltip
+                    tip.html(function() {
+                        return 
$('<div></div>').append(getBackPressureSizeTip(d)).html();
+                    });
+
+                    nfCanvasUtils.canvasTooltip(tip, d3.select(this));
+                });
         }).promise();
 
         // update object count
@@ -1546,24 +1718,65 @@
                         }
                     }
                 }).on('end', function () {
-                backpressurePercentObject
-                    .classed('warning', function (d) {
-                        return isWarningCount(d);
+                    backpressurePercentObject
+                        .classed('warning', function (d) {
+                            return isWarningCount(d);
+                        })
+                        .classed('error', function (d) {
+                            return isErrorCount(d);
+                        });
+
+                    deferred.resolve();
+                });
+
+
+            var backpressurePercentObjectPrediction = 
updated.select('rect.backpressure-tick.object-prediction');
+            backpressurePercentObjectPrediction.transition()
+                .duration(400)
+                .attrs({
+                    'x': function (d) {
+                        // clamp the prediction between 0 and 100 percent
+                        var predicted = _.get(d, 
'status.aggregateSnapshot.predictedPercentCount', 0);
+                        return (backpressureBarWidth * _.clamp(predicted, 0, 
100)) / 100;
+                    },
+                    'display': function (d) {
+                        var predicted = _.get(d, 
'status.aggregateSnapshot.predictedPercentCount', -1);
+                        if (predicted >= 0) {
+                            return 'unset';
+                        } else {
+                            // don't show it if there not a valid prediction
+                            return 'none';
+                        }
+                    }
+                }).on('end', function () {
+                    
backpressurePercentObjectPrediction.classed('prediction-down', function (d) {
+                        var actual = _.get(d, 
'status.aggregateSnapshot.percentUseCount', 0);
+                        var predicted = _.get(d, 
'status.aggregateSnapshot.predictedPercentCount', 0);
+                        return predicted < actual;
                     })
-                    .classed('error', function (d) {
-                        return isErrorCount(d);
-                    });
+                });
 
-                deferred.resolve();
-            });
+            updated.select('g.backpressure-object-container')
+                .each(function(d) {
+                    var tip = d3.select('#back-pressure-count-tip-' + d.id);
 
-            
updated.select('rect.backpressure-object').select('title').text(function (d) {
-                if 
(nfCommon.isDefinedAndNotNull(d.status.aggregateSnapshot.percentUseCount)) {
-                    return 'Queue is ' + 
d.status.aggregateSnapshot.percentUseCount + '% full based on Back Pressure 
Object Threshold';
-                } else {
-                    return 'Back Pressure Object Threshold is not configured';
-                }
-            });
+                    // create a DOM element for the tooltip if ones does not 
already exist
+                    if (tip.empty()) {
+                        tip = d3.select('#connection-tooltips')
+                            .append('div')
+                            .attr('id', function() {
+                                return 'back-pressure-count-tip-' + d.id
+                            })
+                            .attr('class', 'tooltip nifi-tooltip');
+                    }
+
+                    // update the tooltip
+                    tip.html(function() {
+                        return 
$('<div></div>').append(getBackPressureCountTip(d)).html();
+                    });
+
+                    nfCanvasUtils.canvasTooltip(tip, d3.select(this));
+                });
         }).promise();
 
         // update connection once progress bars have transitioned
@@ -1627,7 +1840,15 @@
         });
 
         // remove the connection
-        removed.remove();
+        removed.call(removeTooltips).remove();
+    };
+
+    var removeTooltips = function (removed) {
+        removed.each(function (d) {
+            // remove any associated tooltips
+            $('#back-pressure-size-tip-' + d.id).remove();
+            $('#back-pressure-count-tip-' + d.id).remove();
+        });
     };
 
     var nfConnection = {
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-common.js
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-common.js
index 7e5a1c0..13ee565 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-common.js
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-common.js
@@ -23,22 +23,25 @@
         define(['jquery',
                 'd3',
                 'nf.Storage',
-                'lodash-core'],
-            function ($, d3, nfStorage, _) {
-                return (nf.Common = factory($, d3, nfStorage, _));
+                'lodash-core',
+                'moment'],
+            function ($, d3, nfStorage, _, moment) {
+                return (nf.Common = factory($, d3, nfStorage, _, moment));
             });
     } else if (typeof exports === 'object' && typeof module === 'object') {
         module.exports = (nf.Common = factory(require('jquery'),
             require('d3'),
             require('nf.Storage'),
-            require('lodash-core')));
+            require('lodash-core'),
+            require('moment')));
     } else {
         nf.Common = factory(root.$,
             root.d3,
             root.nf.Storage,
-            root._);
+            root._,
+            root.moment);
     }
-}(this, function ($, d3, nfStorage, _) {
+}(this, function ($, d3, nfStorage, _, moment) {
     'use strict';
 
     $(document).ready(function () {
@@ -1278,6 +1281,13 @@
             }
         },
 
+        formatPredictedDuration: function (duration) {
+            if (duration === 0) {
+                return 'now';
+            }
+            return moment.duration(duration, 'ms').humanize();
+        },
+
         /**
          * Constants for formatting data size.
          */
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/summary/nf-summary-table.js
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/summary/nf-summary-table.js
index 3f2fd1d..4f02e74 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/summary/nf-summary-table.js
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/summary/nf-summary-table.js
@@ -25,9 +25,10 @@
                 'nf.StatusHistory',
                 'nf.ProcessorDetails',
                 'nf.ConnectionDetails',
-                'nf.ng.Bridge'],
-            function ($, Slick, nfCommon, nfErrorHandler, nfStatusHistory, 
nfProcessorDetails, nfConnectionDetails, nfNgBridge) {
-                return (nf.SummaryTable = factory($, Slick, nfCommon, 
nfErrorHandler, nfStatusHistory, nfProcessorDetails, nfConnectionDetails, 
nfNgBridge));
+                'nf.ng.Bridge',
+                'lodash-core'],
+            function ($, Slick, nfCommon, nfErrorHandler, nfStatusHistory, 
nfProcessorDetails, nfConnectionDetails, nfNgBridge, _) {
+                return (nf.SummaryTable = factory($, Slick, nfCommon, 
nfErrorHandler, nfStatusHistory, nfProcessorDetails, nfConnectionDetails, 
nfNgBridge, _));
             });
     } else if (typeof exports === 'object' && typeof module === 'object') {
         module.exports = (nf.SummaryTable =
@@ -38,7 +39,8 @@
                 require('nf.StatusHistory'),
                 require('nf.ProcessorDetails'),
                 require('nf.ConnectionDetails'),
-                require('nf.ng.Bridge')));
+                require('nf.ng.Bridge'),
+                require('lodash-core')));
     } else {
         nf.SummaryTable = factory(root.$,
             root.Slick,
@@ -47,9 +49,10 @@
             root.nf.StatusHistory,
             root.nf.ProcessorDetails,
             root.nf.ConnectionDetails,
-            root.nf.ng.Bridge);
+            root.nf.ng.Bridge,
+            root._);
     }
-}(this, function ($, Slick, nfCommon, nfErrorHandler, nfStatusHistory, 
nfProcessorDetails, nfConnectionDetails, nfNgBridge) {
+}(this, function ($, Slick, nfCommon, nfErrorHandler, nfStatusHistory, 
nfProcessorDetails, nfConnectionDetails, nfNgBridge, _) {
     'use strict';
 
     /**
@@ -64,6 +67,8 @@
         }
     };
 
+    var DATA_SEPARATOR = '&nbsp;&nbsp;|&nbsp;&nbsp;';
+
     /**
      * Goes to the specified component if possible.
      *
@@ -291,12 +296,12 @@
 
         // formatter for io
         var ioFormatter = function (row, cell, value, columnDef, dataContext) {
-            return nfCommon.escapeHtml(dataContext.read) + ' / ' + 
nfCommon.escapeHtml(dataContext.written);
+            return nfCommon.escapeHtml(dataContext.read) + DATA_SEPARATOR + 
nfCommon.escapeHtml(dataContext.written);
         };
 
         // formatter for tasks
         var taskTimeFormatter = function (row, cell, value, columnDef, 
dataContext) {
-            return nfCommon.formatInteger(dataContext.tasks) + ' / ' + 
nfCommon.escapeHtml(dataContext.tasksDuration);
+            return nfCommon.formatInteger(dataContext.tasks) + DATA_SEPARATOR 
+ nfCommon.escapeHtml(dataContext.tasksDuration);
         };
 
         // function for formatting the last accessed time
@@ -309,7 +314,7 @@
             var threadCounts = '';
             var threadTip = '';
             if (dataContext.terminatedThreadCount > 0) {
-                threadCounts = '(' + dataContext.activeThreadCount + ' / ' + 
dataContext.terminatedThreadCount + ')';
+                threadCounts = '(' + dataContext.activeThreadCount + 
DATA_SEPARATOR + dataContext.terminatedThreadCount + ')';
                 threadTip = 'Threads: (Active / Terminated)';
             } else if (dataContext.activeThreadCount > 0) {
                 threadCounts = '(' + dataContext.activeThreadCount + ')';
@@ -372,7 +377,7 @@
         var inputColumn = {
             id: 'input',
             field: 'input',
-            name: '<span class="input-title">In</span>&nbsp;/&nbsp;<span 
class="input-size-title">Size</span>&nbsp;<span style="font-weight: normal; 
overflow: hidden;">5 min</span>',
+            name: '<span class="input-title">In</span>&nbsp;(<span 
class="input-size-title">Size</span>)&nbsp;<span style="font-weight: normal; 
overflow: hidden;">5 min</span>',
             toolTip: 'Count / data size in the last 5 min',
             sortable: true,
             defaultSortAsc: false,
@@ -382,7 +387,7 @@
         var ioColumn = {
             id: 'io',
             field: 'io',
-            name: '<span class="read-title">Read</span>&nbsp;/&nbsp;<span 
class="written-title">Write</span>&nbsp;<span style="font-weight: normal; 
overflow: hidden;">5 min</span>',
+            name: '<span class="read-title">Read</span>' + DATA_SEPARATOR + 
'<span class="written-title">Write</span>&nbsp;<span style="font-weight: 
normal; overflow: hidden;">5 min</span>',
             toolTip: 'Data size in the last 5 min',
             formatter: ioFormatter,
             sortable: true,
@@ -392,7 +397,7 @@
         var outputColumn = {
             id: 'output',
             field: 'output',
-            name: '<span class="output-title">Out</span>&nbsp;/&nbsp;<span 
class="output-size-title">Size</span>&nbsp;<span style="font-weight: normal; 
overflow: hidden;">5 min</span>',
+            name: '<span class="output-title">Out</span>&nbsp;(<span 
class="output-size-title">Size</span>)&nbsp;<span style="font-weight: normal; 
overflow: hidden;">5 min</span>',
             toolTip: 'Count / data size in the last 5 min',
             sortable: true,
             defaultSortAsc: false,
@@ -402,7 +407,7 @@
         var tasksTimeColumn = {
             id: 'tasks',
             field: 'tasks',
-            name: '<span class="tasks-title">Tasks</span>&nbsp;/&nbsp;<span 
class="time-title">Time</span>&nbsp;<span style="font-weight: normal; overflow: 
hidden;">5 min</span>',
+            name: '<span class="tasks-title">Tasks</span>' + DATA_SEPARATOR + 
'<span class="time-title">Time</span>&nbsp;<span style="font-weight: normal; 
overflow: hidden;">5 min</span>',
             toolTip: 'Count / duration in the last 5 min',
             formatter: taskTimeFormatter,
             sortable: true,
@@ -708,23 +713,58 @@
             return '<div class="pointer show-connection-details fa 
fa-info-circle" title="View Connection Details"></div>';
         };
 
+        var formatPercent = function (value) {
+            return _.isNumber(value) && value >= 0 ? _.clamp(value, 0, 100) + 
'%' : 'NA';
+        };
+
         var backpressureFormatter = function (row, cell, value, columnDef, 
dataContext) {
             var percentUseCount = 'NA';
             if (nfCommon.isDefinedAndNotNull(dataContext.percentUseCount)) {
-                percentUseCount = dataContext.percentUseCount + '%';
+                percentUseCount = formatPercent(dataContext.percentUseCount);
             }
             var percentUseBytes = 'NA';
             if (nfCommon.isDefinedAndNotNull(dataContext.percentUseBytes)) {
-                percentUseBytes = dataContext.percentUseBytes + '%';
+                percentUseBytes = formatPercent(dataContext.percentUseBytes);
+            }
+            return nfCommon.escapeHtml(percentUseCount) + DATA_SEPARATOR + 
nfCommon.escapeHtml(percentUseBytes);
+        };
+
+        var backpressurePredictionFormatter = function (row, cell, value, 
columnDef, dataContext) {
+            var predictedMillisUntilBytesBackpressure = _.get(dataContext, 
'predictedMillisUntilBytesBackpressure', -1);
+            var predictedMillisUntilCountBackpressure = _.get(dataContext, 
'predictedMillisUntilCountBackpressure', -1);
+
+            var percentUseCount = _.get(dataContext, 'percentUseCount', 0);
+            var percentUseBytes = _.get(dataContext, 'percentUseBytes', 0);
+
+            var predictions = [
+                { label: 'object', timeToBackPressure: 
predictedMillisUntilCountBackpressure },
+                { label: 'size', timeToBackPressure: 
predictedMillisUntilBytesBackpressure },
+            ];
+            var actualQueuePercents = [
+                { label: 'object', percent: percentUseCount },
+                { label: 'size', percent: percentUseBytes }
+            ];
+
+            var minPrediction = _.minBy(predictions, 'timeToBackPressure');
+            var maxActual = _.maxBy(actualQueuePercents, 'percent');
+
+            if (maxActual.percent >= 100) {
+                // currently experiencing back pressure
+                return 'now (' + maxActual.label + ')';
+            } else if (minPrediction.timeToBackPressure < 0) {
+                // there is not a valid time-to-back-pressure prediction 
available
+                return 'NA';
             }
-            return nfCommon.escapeHtml(percentUseCount) + ' / ' + 
nfCommon.escapeHtml(percentUseBytes);
+
+            var formatted = 
nfCommon.formatPredictedDuration(minPrediction.timeToBackPressure);
+            return nfCommon.escapeHtml(formatted) + ' (' + minPrediction.label 
+ ')';
         };
 
         // define the input, read, written, and output columns (reused between 
both tables)
         var queueColumn = {
             id: 'queued',
             field: 'queued',
-            name: '<span class="queued-title">Queue</span>&nbsp;/&nbsp;<span 
class="queued-size-title">Size</span>',
+            name: '<span class="queued-title">Queue</span>&nbsp;(<span 
class="queued-size-title">Size</span>)',
             sortable: true,
             defaultSortAsc: false,
             resize: true,
@@ -735,13 +775,25 @@
         var backpressureColumn = {
             id: 'backpressure',
             field: 'backpressure',
-            name: '<span 
class="backpressure-object-title">Queue</span>&nbsp;/&nbsp;<span 
class="backpressure-data-size-title">Size</span> Threshold',
+            name: 'Threshold %: <span 
class="backpressure-object-title">Queue</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span 
class="backpressure-data-size-title">Size</span>',
             sortable: true,
             defaultSortAsc: false,
             formatter: backpressureFormatter,
             resize: true
         };
 
+        // define the column used to display backpressure predicted values 
(reused in both tables)
+        var backpressurePredictionColumn = {
+            id: 'backpressurePrediction',
+            field: 'backpressurePrediction',
+            name: 'Estimated Time to Back Pressure',
+            sortable: true,
+            defaultSortAsc: false,
+            formatter: backpressurePredictionFormatter,
+            resize: true,
+            toolTip: 'Estimated Time to Back Pressure'
+        };
+
         // define the column model for the summary table
         var connectionsColumnModel = [
             {
@@ -754,14 +806,6 @@
                 maxWidth: 50
             },
             {
-                id: 'sourceName',
-                field: 'sourceName',
-                name: 'Source Name',
-                sortable: true,
-                resizable: true,
-                formatter: nfCommon.genericValueFormatter
-            },
-            {
                 id: 'name',
                 field: 'name',
                 name: 'Name',
@@ -769,18 +813,27 @@
                 resizable: true,
                 formatter: valueFormatter
             },
+            queueColumn,
+            backpressureColumn,
+            backpressurePredictionColumn,
+            inputColumn,
+            {
+                id: 'sourceName',
+                field: 'sourceName',
+                name: 'From Source',
+                sortable: true,
+                resizable: true,
+                formatter: nfCommon.genericValueFormatter
+            },
+            outputColumn,
             {
                 id: 'destinationName',
                 field: 'destinationName',
-                name: 'Destination Name',
+                name: 'To Destination',
                 sortable: true,
                 resizable: true,
                 formatter: nfCommon.genericValueFormatter
-            },
-            inputColumn,
-            queueColumn,
-            backpressureColumn,
-            outputColumn
+            }
         ];
 
         // add an action column if appropriate
@@ -952,9 +1005,10 @@
                 resizable: true,
                 formatter: nfCommon.genericValueFormatter
             },
-            inputColumn,
             queueColumn,
             backpressureColumn,
+            backpressurePredictionColumn,
+            inputColumn,
             outputColumn
         ];
 
@@ -1032,7 +1086,7 @@
         var transferredColumn = {
             id: 'transferred',
             field: 'transferred',
-            name: '<span 
class="transferred-title">Transferred</span>&nbsp;/&nbsp;<span 
class="transferred-size-title">Size</span>&nbsp;<span style="font-weight: 
normal; overflow: hidden;">5 min</span>',
+            name: '<span 
class="transferred-title">Transferred</span>&nbsp;(<span 
class="transferred-size-title">Size</span>)&nbsp;<span style="font-weight: 
normal; overflow: hidden;">5 min</span>',
             toolTip: 'Count / data size transferred to and from connections in 
the last 5 min',
             resizable: true,
             defaultSortAsc: false,
@@ -1042,7 +1096,7 @@
         var sentColumn = {
             id: 'sent',
             field: 'sent',
-            name: '<span class="sent-title">Sent</span>&nbsp;/&nbsp;<span 
class="sent-size-title">Size</span>&nbsp;<span style="font-weight: normal; 
overflow: hidden;">5 min</span>',
+            name: '<span class="sent-title">Sent</span>&nbsp;(<span 
class="sent-size-title">Size</span>)&nbsp;<span style="font-weight: normal; 
overflow: hidden;">5 min</span>',
             toolTip: 'Count / data size in the last 5 min',
             sortable: true,
             defaultSortAsc: false,
@@ -1052,7 +1106,7 @@
         var receivedColumn = {
             id: 'received',
             field: 'received',
-            name: '<span 
class="received-title">Received</span>&nbsp;/&nbsp;<span 
class="received-size-title">Size</span>&nbsp;<span style="font-weight: normal; 
overflow: hidden;">5 min</span>',
+            name: '<span class="received-title">Received</span>&nbsp;(<span 
class="received-size-title">Size</span>)&nbsp;<span style="font-weight: normal; 
overflow: hidden;">5 min</span>',
             toolTip: 'Count / data size in the last 5 min',
             sortable: true,
             defaultSortAsc: false,
@@ -2296,6 +2350,10 @@
                     var bPercentUseDataSize = 
nfCommon.isDefinedAndNotNull(b['percentUseBytes']) ? b['percentUseBytes'] : -1;
                     return aPercentUseDataSize - bPercentUseDataSize;
                 }
+            } else if (sortDetails.columnId === 'backpressurePrediction') {
+                var aMinTime = Math.min(_.get(a, 
'predictedMillisUntilBytesBackpressure', Number.MAX_VALUE), _.get(a, 
'predictedMillisUntilCountBackpressure', Number.MAX_VALUE));
+                var bMinTime = Math.min(_.get(b, 
'predictedMillisUntilBytesBackpressure', Number.MAX_VALUE), _.get(b, 
'predictedMillisUntilCountBackpressure', Number.MAX_VALUE));
+                return aMinTime - bMinTime;
             } else if (sortDetails.columnId === 'sent' || sortDetails.columnId 
=== 'received' || sortDetails.columnId === 'input' || sortDetails.columnId === 
'output' || sortDetails.columnId === 'transferred') {
                 var aSplit = a[sortDetails.columnId].split(/\(([^)]+)\)/);
                 var bSplit = b[sortDetails.columnId].split(/\(([^)]+)\)/);
@@ -2349,6 +2407,9 @@
         $('#' + tableId + ' span.queued-size-title').removeClass('sorted');
         $('#' + tableId + ' 
span.backpressure-object-title').removeClass('sorted');
         $('#' + tableId + ' 
span.backpressure-data-size-title').removeClass('sorted');
+        $('#' + tableId + ' 
span.backpressure-prediction-object-title').removeClass('sorted');
+        $('#' + tableId + ' 
span.backpressure-prediction-data-size-title').removeClass('sorted');
+        $('#' + tableId + ' 
span.backpressure-prediction-time-title').removeClass('sorted');
         $('#' + tableId + ' span.input-title').removeClass('sorted');
         $('#' + tableId + ' span.input-size-title').removeClass('sorted');
         $('#' + tableId + ' span.output-title').removeClass('sorted');
@@ -2776,6 +2837,13 @@
                         queuedSize: snapshot.queuedSize,
                         percentUseCount: snapshot.percentUseCount,
                         percentUseBytes: snapshot.percentUseBytes,
+                        predictedPercentBytes: snapshot.predictedPercentBytes,
+                        predictedPercentCount: snapshot.predictedPercentCount,
+                        predictedBytesAtNextInterval: 
snapshot.predictedBytesAtNextInterval,
+                        predictedCountAtNextInterval: 
snapshot.predictedCountAtNextInterval,
+                        predictedMillisUntilBytesBackpressure: 
snapshot.predictedMillisUntilBytesBackpressure,
+                        predictedMillisUntilCountBackpressure: 
snapshot.predictedMillisUntilCountBackpressure,
+                        predictionIntervalSeconds: 
snapshot.predictionIntervalSeconds,
                         output: snapshot.output
                     });
                 });

Reply via email to