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

dlmarion pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/accumulo.git


The following commit(s) were added to refs/heads/main by this push:
     new 0390490580 Adds a Column selector button to the tables with dynamic 
content (#6392)
0390490580 is described below

commit 0390490580a31e0b9311dedba90e88fc373f58db
Author: Dave Marion <[email protected]>
AuthorDate: Thu May 28 12:47:35 2026 -0400

    Adds a Column selector button to the tables with dynamic content (#6392)
    
    This change installs the DataTables Buttons and ColVis extensions,
    and includes that button on the DataTables that are created by
    the server_process_common.js file. These tables typically have
    a lot of columns and this button will allow the user to select
    which columns they want to see. The new button is to the right
    of the search bar with a gear icon. Column visibility is retained
    locally so new browser sessions will only show the columns
    selected by the user.
---
 LICENSE                                            |    8 +
 assemble/src/main/resources/LICENSE                |   16 +
 .../src/main/appended-resources/META-INF/LICENSE   |   16 +
 .../external/datatables/css/buttons.bootstrap5.css |  387 +++
 .../external/datatables/js/buttons.bootstrap5.js   |  117 +
 .../external/datatables/js/buttons.colVis.js       |  260 ++
 .../external/datatables/js/dataTables.buttons.js   | 2592 ++++++++++++++++++++
 .../monitor/resources/js/server_process_common.js  |    8 +
 .../accumulo/monitor/templates/compactors.ftl      |    8 +-
 .../accumulo/monitor/templates/coordinator.ftl     |   16 +-
 .../apache/accumulo/monitor/templates/default.ftl  |    4 +
 .../org/apache/accumulo/monitor/templates/gc.ftl   |   11 +-
 .../apache/accumulo/monitor/templates/manager.ftl  |    6 +-
 .../apache/accumulo/monitor/templates/sservers.ftl |    8 +-
 .../apache/accumulo/monitor/templates/tservers.ftl |    8 +-
 15 files changed, 3444 insertions(+), 21 deletions(-)

diff --git a/LICENSE b/LICENSE
index c71ceabecb..d84d8e37e1 100644
--- a/LICENSE
+++ b/LICENSE
@@ -309,6 +309,14 @@ Files (in server/monitor/src/main/resources/):
     Copyright (c) 2008-2024 SpryMedia Ltd
     Licensed under the MIT license (see above)
 
+## DataTables Buttons 2.4.2 (https://datatables.net)
+
+Files (in server/monitor/src/main/resources/):
+* org/apache/accumulo/monitor/resources/external/datatables/**/*
+
+    Copyright (c) 2010-2015 SpryMedia Limited
+    Licensed under the MIT license (see above)
+
 ## DataTables ColReorder 1.7.0 (https://datatables.net)
 
 Files (in server/monitor/src/main/resources/):
diff --git a/assemble/src/main/resources/LICENSE 
b/assemble/src/main/resources/LICENSE
index caf46e5357..d6bd358baf 100644
--- a/assemble/src/main/resources/LICENSE
+++ b/assemble/src/main/resources/LICENSE
@@ -309,6 +309,22 @@ Files (in lib/accumulo-monitor-*.jar):
     Copyright (c) 2008-2024 SpryMedia Ltd
     Licensed under the MIT license (see above)
 
+## DataTables Buttons 2.4.2 (https://datatables.net)
+
+Files (in server/monitor/src/main/resources/):
+* org/apache/accumulo/monitor/resources/external/datatables/**/*
+
+    Copyright (c) 2010-2015 SpryMedia Limited
+    Licensed under the MIT license (see above)
+
+## DataTables ColReorder 1.7.0 (https://datatables.net)
+
+Files (in server/monitor/src/main/resources/):
+* org/apache/accumulo/monitor/resources/external/datatables/**/*
+
+    Copyright (c) 2010-2015 SpryMedia Limited
+    Licensed under the MIT license (see above)
+
 **********
 
 # BUNDLED DEPENDENCIES
diff --git a/server/monitor/src/main/appended-resources/META-INF/LICENSE 
b/server/monitor/src/main/appended-resources/META-INF/LICENSE
index 39f0ba8551..11f0d3ced7 100644
--- a/server/monitor/src/main/appended-resources/META-INF/LICENSE
+++ b/server/monitor/src/main/appended-resources/META-INF/LICENSE
@@ -71,4 +71,20 @@ Files (in lib/accumulo-monitor-*.jar):
     Copyright (c) 2008-2024 SpryMedia Ltd
     Licensed under the MIT license (see above)
 
+## DataTables Buttons 2.4.2 (https://datatables.net)
+
+Files (in server/monitor/src/main/resources/):
+* org/apache/accumulo/monitor/resources/external/datatables/**/*
+
+    Copyright (c) 2010-2015 SpryMedia Limited
+    Licensed under the MIT license (see above)
+
+## DataTables ColReorder 1.7.0 (https://datatables.net)
+
+Files (in server/monitor/src/main/resources/):
+* org/apache/accumulo/monitor/resources/external/datatables/**/*
+
+    Copyright (c) 2010-2015 SpryMedia Limited
+    Licensed under the MIT license (see above)
+
 **********
diff --git 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/css/buttons.bootstrap5.css
 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/css/buttons.bootstrap5.css
new file mode 100644
index 0000000000..df249f01a1
--- /dev/null
+++ 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/css/buttons.bootstrap5.css
@@ -0,0 +1,387 @@
+@keyframes dtb-spinner {
+  100% {
+    transform: rotate(360deg);
+  }
+}
+@-o-keyframes dtb-spinner {
+  100% {
+    -o-transform: rotate(360deg);
+    transform: rotate(360deg);
+  }
+}
+@-ms-keyframes dtb-spinner {
+  100% {
+    -ms-transform: rotate(360deg);
+    transform: rotate(360deg);
+  }
+}
+@-webkit-keyframes dtb-spinner {
+  100% {
+    -webkit-transform: rotate(360deg);
+    transform: rotate(360deg);
+  }
+}
+@-moz-keyframes dtb-spinner {
+  100% {
+    -moz-transform: rotate(360deg);
+    transform: rotate(360deg);
+  }
+}
+div.dataTables_wrapper {
+  position: relative;
+}
+
+div.dt-buttons {
+  position: initial;
+}
+div.dt-buttons .dt-button {
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+div.dt-button-info {
+  position: fixed;
+  top: 50%;
+  left: 50%;
+  width: 400px;
+  margin-top: -100px;
+  margin-left: -200px;
+  background-color: white;
+  border-radius: 0.75em;
+  box-shadow: 3px 4px 10px 1px rgba(0, 0, 0, 0.8);
+  text-align: center;
+  z-index: 2003;
+  overflow: hidden;
+}
+div.dt-button-info h2 {
+  padding: 2rem 2rem 1rem 2rem;
+  margin: 0;
+  font-weight: normal;
+}
+div.dt-button-info > div {
+  padding: 1em 2em 2em 2em;
+}
+
+div.dtb-popover-close {
+  position: absolute;
+  top: 6px;
+  right: 6px;
+  width: 22px;
+  height: 22px;
+  text-align: center;
+  border-radius: 3px;
+  cursor: pointer;
+  z-index: 2003;
+}
+
+button.dtb-hide-drop {
+  display: none !important;
+}
+
+div.dt-button-collection-title {
+  text-align: center;
+  padding: 0.3em 0 0.5em;
+  margin-left: 0.5em;
+  margin-right: 0.5em;
+  font-size: 0.9em;
+}
+
+div.dt-button-collection-title:empty {
+  display: none;
+}
+
+span.dt-button-spacer {
+  display: inline-block;
+  margin: 0.5em;
+  white-space: nowrap;
+}
+span.dt-button-spacer.bar {
+  border-left: 1px solid rgba(0, 0, 0, 0.3);
+  vertical-align: middle;
+  padding-left: 0.5em;
+}
+span.dt-button-spacer.bar:empty {
+  height: 1em;
+  width: 1px;
+  padding-left: 0;
+}
+
+div.dt-button-collection .dt-button-active {
+  padding-right: 3em;
+}
+div.dt-button-collection .dt-button-active:after {
+  position: absolute;
+  top: 50%;
+  margin-top: -10px;
+  right: 1em;
+  display: inline-block;
+  content: "✓";
+  color: inherit;
+}
+div.dt-button-collection .dt-button-active.dt-button-split {
+  padding-right: 0;
+}
+div.dt-button-collection .dt-button-active.dt-button-split:after {
+  display: none;
+}
+div.dt-button-collection .dt-button-active.dt-button-split > *:first-child {
+  padding-right: 3em;
+}
+div.dt-button-collection .dt-button-active.dt-button-split > 
*:first-child:after {
+  position: absolute;
+  top: 50%;
+  margin-top: -10px;
+  right: 1em;
+  display: inline-block;
+  content: "✓";
+  color: inherit;
+}
+div.dt-button-collection .dt-button-active-a a {
+  padding-right: 3em;
+}
+div.dt-button-collection .dt-button-active-a a:after {
+  position: absolute;
+  right: 1em;
+  display: inline-block;
+  content: "✓";
+  color: inherit;
+}
+div.dt-button-collection span.dt-button-spacer {
+  width: 100%;
+  font-size: 0.9em;
+  text-align: center;
+  margin: 0.5em 0;
+}
+div.dt-button-collection span.dt-button-spacer:empty {
+  height: 0;
+  width: 100%;
+}
+div.dt-button-collection span.dt-button-spacer.bar {
+  border-left: none;
+  border-bottom: 1px solid rgba(0, 0, 0, 0.1);
+  padding-left: 0;
+}
+
+html.dark div.dt-button-info {
+  background-color: var(--dt-html-background);
+  border: 1px solid rgba(255, 255, 255, 0.15);
+}
+
+div.dt-buttons div.btn-group {
+  position: initial;
+}
+div.dt-buttons div.dropdown-menu {
+  margin-top: 4px;
+}
+div.dt-buttons div.dropdown-menu .dt-button {
+  position: relative;
+}
+div.dt-buttons div.dropdown-menu div.dt-button-split {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  justify-content: flex-start;
+  align-content: flex-start;
+  align-items: stretch;
+}
+div.dt-buttons div.dropdown-menu div.dt-button-split a:first-child {
+  min-width: auto;
+  flex: 1 0 50px;
+  padding-right: 0;
+}
+div.dt-buttons div.dropdown-menu div.dt-button-split button:last-child {
+  min-width: 33px;
+  flex: 0;
+  background: transparent;
+  border: none;
+  line-height: 1rem;
+  color: var(--bs-dropdown-link-color);
+  padding: var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);
+}
+div.dt-buttons div.dropdown-menu div.dt-button-split button:last-child:hover {
+  color: var(--bs-dropdown-link-hover-color);
+  background-color: var(--bs-dropdown-link-hover-bg);
+}
+div.dt-buttons div.dropdown-menu div.dt-button-split button:last-child:after {
+  position: relative;
+  left: -3px;
+}
+div.dt-buttons div.dropdown-menu.fixed {
+  position: fixed;
+  display: block;
+  top: 50%;
+  left: 50%;
+  margin-left: -75px;
+  border-radius: 5px;
+  background-color: white;
+  padding: 0.5em;
+}
+div.dt-buttons div.dropdown-menu.fixed.two-column {
+  margin-left: -200px;
+}
+div.dt-buttons div.dropdown-menu.fixed.three-column {
+  margin-left: -225px;
+}
+div.dt-buttons div.dropdown-menu.fixed.four-column {
+  margin-left: -300px;
+}
+div.dt-buttons div.dropdown-menu.fixed.columns {
+  margin-left: -409px;
+}
+@media screen and (max-width: 1024px) {
+  div.dt-buttons div.dropdown-menu.fixed.columns {
+    margin-left: -308px;
+  }
+}
+@media screen and (max-width: 640px) {
+  div.dt-buttons div.dropdown-menu.fixed.columns {
+    margin-left: -203px;
+  }
+}
+@media screen and (max-width: 460px) {
+  div.dt-buttons div.dropdown-menu.fixed.columns {
+    margin-left: -100px;
+  }
+}
+div.dt-buttons div.dropdown-menu.fixed > :last-child {
+  max-height: 100vh;
+  overflow: auto;
+}
+div.dt-buttons div.dropdown-menu.two-column > :last-child, div.dt-buttons 
div.dropdown-menu.three-column > :last-child, div.dt-buttons 
div.dropdown-menu.four-column > :last-child {
+  display: block !important;
+  -webkit-column-gap: 8px;
+  -moz-column-gap: 8px;
+  -ms-column-gap: 8px;
+  -o-column-gap: 8px;
+  column-gap: 8px;
+}
+div.dt-buttons div.dropdown-menu.two-column > :last-child > *, div.dt-buttons 
div.dropdown-menu.three-column > :last-child > *, div.dt-buttons 
div.dropdown-menu.four-column > :last-child > * {
+  -webkit-column-break-inside: avoid;
+  break-inside: avoid;
+}
+div.dt-buttons div.dropdown-menu.two-column {
+  width: 400px;
+}
+div.dt-buttons div.dropdown-menu.two-column > :last-child {
+  padding-bottom: 1px;
+  column-count: 2;
+}
+div.dt-buttons div.dropdown-menu.three-column {
+  width: 450px;
+}
+div.dt-buttons div.dropdown-menu.three-column > :last-child {
+  padding-bottom: 1px;
+  column-count: 3;
+}
+div.dt-buttons div.dropdown-menu.four-column {
+  width: 600px;
+}
+div.dt-buttons div.dropdown-menu.four-column > :last-child {
+  padding-bottom: 1px;
+  column-count: 4;
+}
+div.dt-buttons div.dropdown-menu .dt-button {
+  border-radius: 0;
+}
+div.dt-buttons div.dropdown-menu.columns {
+  width: auto;
+}
+div.dt-buttons div.dropdown-menu.columns > :last-child {
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: flex-start;
+  align-items: center;
+  gap: 6px;
+  width: 818px;
+  padding-bottom: 1px;
+}
+div.dt-buttons div.dropdown-menu.columns > :last-child .dt-button {
+  min-width: 200px;
+  flex: 0 1;
+  margin: 0;
+}
+div.dt-buttons div.dropdown-menu.columns.dtb-b3 > :last-child, div.dt-buttons 
div.dropdown-menu.columns.dtb-b2 > :last-child, div.dt-buttons 
div.dropdown-menu.columns.dtb-b1 > :last-child {
+  justify-content: space-between;
+}
+div.dt-buttons div.dropdown-menu.columns.dtb-b3 .dt-button {
+  flex: 1 1 32%;
+}
+div.dt-buttons div.dropdown-menu.columns.dtb-b2 .dt-button {
+  flex: 1 1 48%;
+}
+div.dt-buttons div.dropdown-menu.columns.dtb-b1 .dt-button {
+  flex: 1 1 100%;
+}
+@media screen and (max-width: 1024px) {
+  div.dt-buttons div.dropdown-menu.columns > :last-child {
+    width: 612px;
+  }
+}
+@media screen and (max-width: 640px) {
+  div.dt-buttons div.dropdown-menu.columns > :last-child {
+    width: 406px;
+  }
+  div.dt-buttons div.dropdown-menu.columns.dtb-b3 .dt-button {
+    flex: 0 1 32%;
+  }
+}
+@media screen and (max-width: 460px) {
+  div.dt-buttons div.dropdown-menu.columns > :last-child {
+    width: 200px;
+  }
+}
+div.dt-buttons span.dt-button-spacer.empty {
+  margin: 1px;
+}
+div.dt-buttons span.dt-button-spacer.bar:empty {
+  height: inherit;
+}
+div.dt-buttons .btn.processing {
+  color: rgba(0, 0, 0, 0.2);
+}
+div.dt-buttons .btn.processing:after {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  width: 16px;
+  height: 16px;
+  margin: -8px 0 0 -8px;
+  box-sizing: border-box;
+  display: block;
+  content: " ";
+  border: 2px solid rgb(40, 40, 40);
+  border-radius: 50%;
+  border-left-color: transparent;
+  border-right-color: transparent;
+  animation: dtb-spinner 1500ms infinite linear;
+  -o-animation: dtb-spinner 1500ms infinite linear;
+  -ms-animation: dtb-spinner 1500ms infinite linear;
+  -webkit-animation: dtb-spinner 1500ms infinite linear;
+  -moz-animation: dtb-spinner 1500ms infinite linear;
+}
+
+div.dt-button-background {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  z-index: 999;
+}
+
+@media screen and (max-width: 767px) {
+  div.dt-buttons {
+    float: none;
+    width: 100%;
+    text-align: center;
+    margin-bottom: 0.5em;
+  }
+  div.dt-buttons a.btn {
+    float: none;
+  }
+}
+:root[data-bs-theme=dark] div.dropdown-menu.dt-button-collection.fixed {
+  background-color: rgb(33, 37, 41);
+  border: 1px solid rgba(255, 255, 255, 0.15);
+  border-radius: 8px;
+}
diff --git 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/js/buttons.bootstrap5.js
 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/js/buttons.bootstrap5.js
new file mode 100644
index 0000000000..e3f41e8c0e
--- /dev/null
+++ 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/js/buttons.bootstrap5.js
@@ -0,0 +1,117 @@
+/*! Bootstrap integration for DataTables' Buttons
+ * © SpryMedia Ltd - datatables.net/license
+ */
+
+(function( factory ){
+       if ( typeof define === 'function' && define.amd ) {
+               // AMD
+               define( ['jquery', 'datatables.net-bs5', 
'datatables.net-buttons'], function ( $ ) {
+                       return factory( $, window, document );
+               } );
+       }
+       else if ( typeof exports === 'object' ) {
+               // CommonJS
+               var jq = require('jquery');
+               var cjsRequires = function (root, $) {
+                       if ( ! $.fn.dataTable ) {
+                               require('datatables.net-bs5')(root, $);
+                       }
+
+                       if ( ! $.fn.dataTable.Buttons ) {
+                               require('datatables.net-buttons')(root, $);
+                       }
+               };
+
+               if (typeof window === 'undefined') {
+                       module.exports = function (root, $) {
+                               if ( ! root ) {
+                                       // CommonJS environments without a 
window global must pass a
+                                       // root. This will give an error 
otherwise
+                                       root = window;
+                               }
+
+                               if ( ! $ ) {
+                                       $ = jq( root );
+                               }
+
+                               cjsRequires( root, $ );
+                               return factory( $, root, root.document );
+                       };
+               }
+               else {
+                       cjsRequires( window, jq );
+                       module.exports = factory( jq, window, window.document );
+               }
+       }
+       else {
+               // Browser
+               factory( jQuery, window, document );
+       }
+}(function( $, window, document, undefined ) {
+'use strict';
+var DataTable = $.fn.dataTable;
+
+
+
+$.extend(true, DataTable.Buttons.defaults, {
+       dom: {
+               container: {
+                       className: 'dt-buttons btn-group flex-wrap'
+               },
+               button: {
+                       className: 'btn btn-secondary',
+                       active: 'active'
+               },
+               collection: {
+                       action: {
+                               dropHtml: ''
+                       },
+                       container: {
+                               tag: 'div',
+                               className: 'dropdown-menu dt-button-collection'
+                       },
+                       closeButton: false,
+                       button: {
+                               tag: 'a',
+                               className: 'dt-button dropdown-item',
+                               active: 'dt-button-active',
+                               disabled: 'disabled',
+                               spacer: {
+                                       className: 'dropdown-divider',
+                                       tag: 'hr'
+                               }
+                       }
+               },
+               split: {
+                       action: {
+                               tag: 'a',
+                               className: 'btn btn-secondary 
dt-button-split-drop-button',
+                               closeButton: false
+                       },
+                       dropdown: {
+                               tag: 'button',
+                               dropHtml: '',
+                               className:
+                                       'btn btn-secondary dt-button-split-drop 
dropdown-toggle dropdown-toggle-split',
+                               closeButton: false,
+                               align: 'split-left',
+                               splitAlignClass: 'dt-button-split-left'
+                       },
+                       wrapper: {
+                               tag: 'div',
+                               className: 'dt-button-split btn-group',
+                               closeButton: false
+                       }
+               }
+       },
+       buttonCreated: function (config, button) {
+               return config.buttons ? $('<div 
class="btn-group"/>').append(button) : button;
+       }
+});
+
+DataTable.ext.buttons.collection.className += ' dropdown-toggle';
+DataTable.ext.buttons.collection.rightAlignClassName = 'dropdown-menu-right';
+
+
+return DataTable;
+}));
diff --git 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/js/buttons.colVis.js
 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/js/buttons.colVis.js
new file mode 100644
index 0000000000..7e8491bd1e
--- /dev/null
+++ 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/js/buttons.colVis.js
@@ -0,0 +1,260 @@
+/*!
+ * Column visibility buttons for Buttons and DataTables.
+ * © SpryMedia Ltd - datatables.net/license
+ */
+
+(function( factory ){
+       if ( typeof define === 'function' && define.amd ) {
+               // AMD
+               define( ['jquery', 'datatables.net', 'datatables.net-buttons'], 
function ( $ ) {
+                       return factory( $, window, document );
+               } );
+       }
+       else if ( typeof exports === 'object' ) {
+               // CommonJS
+               var jq = require('jquery');
+               var cjsRequires = function (root, $) {
+                       if ( ! $.fn.dataTable ) {
+                               require('datatables.net')(root, $);
+                       }
+
+                       if ( ! $.fn.dataTable.Buttons ) {
+                               require('datatables.net-buttons')(root, $);
+                       }
+               };
+
+               if (typeof window === 'undefined') {
+                       module.exports = function (root, $) {
+                               if ( ! root ) {
+                                       // CommonJS environments without a 
window global must pass a
+                                       // root. This will give an error 
otherwise
+                                       root = window;
+                               }
+
+                               if ( ! $ ) {
+                                       $ = jq( root );
+                               }
+
+                               cjsRequires( root, $ );
+                               return factory( $, root, root.document );
+                       };
+               }
+               else {
+                       cjsRequires( window, jq );
+                       module.exports = factory( jq, window, window.document );
+               }
+       }
+       else {
+               // Browser
+               factory( jQuery, window, document );
+       }
+}(function( $, window, document, undefined ) {
+'use strict';
+var DataTable = $.fn.dataTable;
+
+
+
+$.extend(DataTable.ext.buttons, {
+       // A collection of column visibility buttons
+       colvis: function (dt, conf) {
+               var node = null;
+               var buttonConf = {
+                       extend: 'collection',
+                       init: function (dt, n) {
+                               node = n;
+                       },
+                       text: function (dt) {
+                               return dt.i18n('buttons.colvis', 'Column 
visibility');
+                       },
+                       className: 'buttons-colvis',
+                       closeButton: false,
+                       buttons: [
+                               {
+                                       extend: 'columnsToggle',
+                                       columns: conf.columns,
+                                       columnText: conf.columnText
+                               }
+                       ]
+               };
+
+               // Rebuild the collection with the new column structure if 
columns are reordered
+               dt.on('column-reorder.dt' + conf.namespace, function (e, 
settings, details) {
+                       dt.button(null, dt.button(null, 
node).node()).collectionRebuild([
+                               {
+                                       extend: 'columnsToggle',
+                                       columns: conf.columns,
+                                       columnText: conf.columnText
+                               }
+                       ]);
+               });
+
+               return buttonConf;
+       },
+
+       // Selected columns with individual buttons - toggle column visibility
+       columnsToggle: function (dt, conf) {
+               var columns = dt
+                       .columns(conf.columns)
+                       .indexes()
+                       .map(function (idx) {
+                               return {
+                                       extend: 'columnToggle',
+                                       columns: idx,
+                                       columnText: conf.columnText
+                               };
+                       })
+                       .toArray();
+
+               return columns;
+       },
+
+       // Single button to toggle column visibility
+       columnToggle: function (dt, conf) {
+               return {
+                       extend: 'columnVisibility',
+                       columns: conf.columns,
+                       columnText: conf.columnText
+               };
+       },
+
+       // Selected columns with individual buttons - set column visibility
+       columnsVisibility: function (dt, conf) {
+               var columns = dt
+                       .columns(conf.columns)
+                       .indexes()
+                       .map(function (idx) {
+                               return {
+                                       extend: 'columnVisibility',
+                                       columns: idx,
+                                       visibility: conf.visibility,
+                                       columnText: conf.columnText
+                               };
+                       })
+                       .toArray();
+
+               return columns;
+       },
+
+       // Single button to set column visibility
+       columnVisibility: {
+               columns: undefined, // column selector
+               text: function (dt, button, conf) {
+                       return conf._columnText(dt, conf);
+               },
+               className: 'buttons-columnVisibility',
+               action: function (e, dt, button, conf) {
+                       var col = dt.columns(conf.columns);
+                       var curr = col.visible();
+
+                       col.visible(
+                               conf.visibility !== undefined ? conf.visibility 
: !(curr.length ? curr[0] : false)
+                       );
+               },
+               init: function (dt, button, conf) {
+                       var that = this;
+                       button.attr('data-cv-idx', conf.columns);
+
+                       dt.on('column-visibility.dt' + conf.namespace, function 
(e, settings) {
+                               if (!settings.bDestroying && settings.nTable == 
dt.settings()[0].nTable) {
+                                       
that.active(dt.column(conf.columns).visible());
+                               }
+                       }).on('column-reorder.dt' + conf.namespace, function 
(e, settings, details) {
+                               // Button has been removed from the DOM
+                               if (conf.destroying) {
+                                       return;
+                               }
+
+                               if (dt.columns(conf.columns).count() !== 1) {
+                                       return;
+                               }
+
+                               // This button controls the same column index 
but the text for the column has
+                               // changed
+                               that.text(conf._columnText(dt, conf));
+
+                               // Since its a different column, we need to 
check its visibility
+                               that.active(dt.column(conf.columns).visible());
+                       });
+
+                       this.active(dt.column(conf.columns).visible());
+               },
+               destroy: function (dt, button, conf) {
+                       dt.off('column-visibility.dt' + conf.namespace).off(
+                               'column-reorder.dt' + conf.namespace
+                       );
+               },
+
+               _columnText: function (dt, conf) {
+                       // Use DataTables' internal data structure until this 
is presented
+                       // is a public API. The other option is to use
+                       // `$( column(col).node() ).text()` but the node might 
not have been
+                       // populated when Buttons is constructed.
+                       var idx = dt.column(conf.columns).index();
+                       var title = dt.settings()[0].aoColumns[idx].sTitle;
+
+                       if (!title) {
+                               title = dt.column(idx).header().innerHTML;
+                       }
+
+                       title = title
+                               .replace(/\n/g, ' ') // remove new lines
+                               .replace(/<br\s*\/?>/gi, ' ') // replace line 
breaks with spaces
+                               .replace(/<select(.*?)<\/select>/g, '') // 
remove select tags, including options text
+                               .replace(/<!\-\-.*?\-\->/g, '') // strip HTML 
comments
+                               .replace(/<.*?>/g, '') // strip HTML
+                               .replace(/^\s+|\s+$/g, ''); // trim
+
+                       return conf.columnText ? conf.columnText(dt, idx, 
title) : title;
+               }
+       },
+
+       colvisRestore: {
+               className: 'buttons-colvisRestore',
+
+               text: function (dt) {
+                       return dt.i18n('buttons.colvisRestore', 'Restore 
visibility');
+               },
+
+               init: function (dt, button, conf) {
+                       conf._visOriginal = dt
+                               .columns()
+                               .indexes()
+                               .map(function (idx) {
+                                       return dt.column(idx).visible();
+                               })
+                               .toArray();
+               },
+
+               action: function (e, dt, button, conf) {
+                       dt.columns().every(function (i) {
+                               // Take into account that ColReorder might have 
disrupted our
+                               // indexes
+                               var idx =
+                                       dt.colReorder && dt.colReorder.transpose
+                                               ? dt.colReorder.transpose(i, 
'toOriginal')
+                                               : i;
+
+                               this.visible(conf._visOriginal[idx]);
+                       });
+               }
+       },
+
+       colvisGroup: {
+               className: 'buttons-colvisGroup',
+
+               action: function (e, dt, button, conf) {
+                       dt.columns(conf.show).visible(true, false);
+                       dt.columns(conf.hide).visible(false, false);
+
+                       dt.columns.adjust();
+               },
+
+               show: [],
+
+               hide: []
+       }
+});
+
+
+return DataTable;
+}));
diff --git 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/js/dataTables.buttons.js
 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/js/dataTables.buttons.js
new file mode 100644
index 0000000000..d4fd8dc7ba
--- /dev/null
+++ 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/js/dataTables.buttons.js
@@ -0,0 +1,2592 @@
+/*! Buttons for DataTables 2.4.2
+ * © SpryMedia Ltd - datatables.net/license
+ */
+
+(function( factory ){
+       if ( typeof define === 'function' && define.amd ) {
+               // AMD
+               define( ['jquery', 'datatables.net'], function ( $ ) {
+                       return factory( $, window, document );
+               } );
+       }
+       else if ( typeof exports === 'object' ) {
+               // CommonJS
+               var jq = require('jquery');
+               var cjsRequires = function (root, $) {
+                       if ( ! $.fn.dataTable ) {
+                               require('datatables.net')(root, $);
+                       }
+               };
+
+               if (typeof window === 'undefined') {
+                       module.exports = function (root, $) {
+                               if ( ! root ) {
+                                       // CommonJS environments without a 
window global must pass a
+                                       // root. This will give an error 
otherwise
+                                       root = window;
+                               }
+
+                               if ( ! $ ) {
+                                       $ = jq( root );
+                               }
+
+                               cjsRequires( root, $ );
+                               return factory( $, root, root.document );
+                       };
+               }
+               else {
+                       cjsRequires( window, jq );
+                       module.exports = factory( jq, window, window.document );
+               }
+       }
+       else {
+               // Browser
+               factory( jQuery, window, document );
+       }
+}(function( $, window, document, undefined ) {
+'use strict';
+var DataTable = $.fn.dataTable;
+
+
+
+// Used for namespacing events added to the document by each instance, so they
+// can be removed on destroy
+var _instCounter = 0;
+
+// Button namespacing counter for namespacing events on individual buttons
+var _buttonCounter = 0;
+
+var _dtButtons = DataTable.ext.buttons;
+
+// Custom entity decoder for data export
+var _entityDecoder = null;
+
+// Allow for jQuery slim
+function _fadeIn(el, duration, fn) {
+       if ($.fn.animate) {
+               el.stop().fadeIn(duration, fn);
+       }
+       else {
+               el.css('display', 'block');
+
+               if (fn) {
+                       fn.call(el);
+               }
+       }
+}
+
+function _fadeOut(el, duration, fn) {
+       if ($.fn.animate) {
+               el.stop().fadeOut(duration, fn);
+       }
+       else {
+               el.css('display', 'none');
+
+               if (fn) {
+                       fn.call(el);
+               }
+       }
+}
+
+/**
+ * [Buttons description]
+ * @param {[type]}
+ * @param {[type]}
+ */
+var Buttons = function (dt, config) {
+       // If not created with a `new` keyword then we return a wrapper 
function that
+       // will take the settings object for a DT. This allows easy use of new 
instances
+       // with the `layout` option - e.g. `topLeft: $.fn.dataTable.Buttons( 
... )`.
+       if (!(this instanceof Buttons)) {
+               return function (settings) {
+                       return new Buttons(settings, dt).container();
+               };
+       }
+
+       // If there is no config set it to an empty object
+       if (typeof config === 'undefined') {
+               config = {};
+       }
+
+       // Allow a boolean true for defaults
+       if (config === true) {
+               config = {};
+       }
+
+       // For easy configuration of buttons an array can be given
+       if (Array.isArray(config)) {
+               config = { buttons: config };
+       }
+
+       this.c = $.extend(true, {}, Buttons.defaults, config);
+
+       // Don't want a deep copy for the buttons
+       if (config.buttons) {
+               this.c.buttons = config.buttons;
+       }
+
+       this.s = {
+               dt: new DataTable.Api(dt),
+               buttons: [],
+               listenKeys: '',
+               namespace: 'dtb' + _instCounter++
+       };
+
+       this.dom = {
+               container: $('<' + this.c.dom.container.tag + 
'/>').addClass(this.c.dom.container.className)
+       };
+
+       this._constructor();
+};
+
+$.extend(Buttons.prototype, {
+       /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 
* * *
+        * Public methods
+        */
+
+       /**
+        * Get the action of a button
+        * @param  {int|string} Button index
+        * @return {function}
+        */ /**
+        * Set the action of a button
+        * @param  {node} node Button element
+        * @param  {function} action Function to set
+        * @return {Buttons} Self for chaining
+        */
+       action: function (node, action) {
+               var button = this._nodeToButton(node);
+
+               if (action === undefined) {
+                       return button.conf.action;
+               }
+
+               button.conf.action = action;
+
+               return this;
+       },
+
+       /**
+        * Add an active class to the button to make to look active or get 
current
+        * active state.
+        * @param  {node} node Button element
+        * @param  {boolean} [flag] Enable / disable flag
+        * @return {Buttons} Self for chaining or boolean for getter
+        */
+       active: function (node, flag) {
+               var button = this._nodeToButton(node);
+               var klass = this.c.dom.button.active;
+               var jqNode = $(button.node);
+
+               if (
+                       button.inCollection &&
+                       this.c.dom.collection.button &&
+                       this.c.dom.collection.button.active !== undefined
+               ) {
+                       klass = this.c.dom.collection.button.active;
+               }
+
+               if (flag === undefined) {
+                       return jqNode.hasClass(klass);
+               }
+
+               jqNode.toggleClass(klass, flag === undefined ? true : flag);
+
+               return this;
+       },
+
+       /**
+        * Add a new button
+        * @param {object} config Button configuration object, base string name 
or function
+        * @param {int|string} [idx] Button index for where to insert the button
+        * @param {boolean} [draw=true] Trigger a draw. Set a false when adding
+        *   lots of buttons, until the last button.
+        * @return {Buttons} Self for chaining
+        */
+       add: function (config, idx, draw) {
+               var buttons = this.s.buttons;
+
+               if (typeof idx === 'string') {
+                       var split = idx.split('-');
+                       var base = this.s;
+
+                       for (var i = 0, ien = split.length - 1; i < ien; i++) {
+                               base = base.buttons[split[i] * 1];
+                       }
+
+                       buttons = base.buttons;
+                       idx = split[split.length - 1] * 1;
+               }
+
+               this._expandButton(
+                       buttons,
+                       config,
+                       config !== undefined ? config.split : undefined,
+                       (config === undefined || config.split === undefined || 
config.split.length === 0) &&
+                               base !== undefined,
+                       false,
+                       idx
+               );
+
+               if (draw === undefined || draw === true) {
+                       this._draw();
+               }
+
+               return this;
+       },
+
+       /**
+        * Clear buttons from a collection and then insert new buttons
+        */
+       collectionRebuild: function (node, newButtons) {
+               var button = this._nodeToButton(node);
+
+               if (newButtons !== undefined) {
+                       var i;
+                       // Need to reverse the array
+                       for (i = button.buttons.length - 1; i >= 0; i--) {
+                               this.remove(button.buttons[i].node);
+                       }
+
+                       // If the collection has prefix and / or postfix 
buttons we need to add them in
+                       if (button.conf.prefixButtons) {
+                               newButtons.unshift.apply(newButtons, 
button.conf.prefixButtons);
+                       }
+
+                       if (button.conf.postfixButtons) {
+                               newButtons.push.apply(newButtons, 
button.conf.postfixButtons);
+                       }
+
+                       for (i = 0; i < newButtons.length; i++) {
+                               var newBtn = newButtons[i];
+
+                               this._expandButton(
+                                       button.buttons,
+                                       newBtn,
+                                       newBtn !== undefined &&
+                                               newBtn.config !== undefined &&
+                                               newBtn.config.split !== 
undefined,
+                                       true,
+                                       newBtn.parentConf !== undefined && 
newBtn.parentConf.split !== undefined,
+                                       null,
+                                       newBtn.parentConf
+                               );
+                       }
+               }
+
+               this._draw(button.collection, button.buttons);
+       },
+
+       /**
+        * Get the container node for the buttons
+        * @return {jQuery} Buttons node
+        */
+       container: function () {
+               return this.dom.container;
+       },
+
+       /**
+        * Disable a button
+        * @param  {node} node Button node
+        * @return {Buttons} Self for chaining
+        */
+       disable: function (node) {
+               var button = this._nodeToButton(node);
+
+               
$(button.node).addClass(this.c.dom.button.disabled).prop('disabled', true);
+
+               return this;
+       },
+
+       /**
+        * Destroy the instance, cleaning up event handlers and removing DOM
+        * elements
+        * @return {Buttons} Self for chaining
+        */
+       destroy: function () {
+               // Key event listener
+               $('body').off('keyup.' + this.s.namespace);
+
+               // Individual button destroy (so they can remove their own 
events if
+               // needed). Take a copy as the array is modified by `remove`
+               var buttons = this.s.buttons.slice();
+               var i, ien;
+
+               for (i = 0, ien = buttons.length; i < ien; i++) {
+                       this.remove(buttons[i].node);
+               }
+
+               // Container
+               this.dom.container.remove();
+
+               // Remove from the settings object collection
+               var buttonInsts = this.s.dt.settings()[0];
+
+               for (i = 0, ien = buttonInsts.length; i < ien; i++) {
+                       if (buttonInsts.inst === this) {
+                               buttonInsts.splice(i, 1);
+                               break;
+                       }
+               }
+
+               return this;
+       },
+
+       /**
+        * Enable / disable a button
+        * @param  {node} node Button node
+        * @param  {boolean} [flag=true] Enable / disable flag
+        * @return {Buttons} Self for chaining
+        */
+       enable: function (node, flag) {
+               if (flag === false) {
+                       return this.disable(node);
+               }
+
+               var button = this._nodeToButton(node);
+               
$(button.node).removeClass(this.c.dom.button.disabled).prop('disabled', false);
+
+               return this;
+       },
+
+       /**
+        * Get a button's index
+        *
+        * This is internally recursive
+        * @param {element} node Button to get the index of
+        * @return {string} Button index
+        */
+       index: function (node, nested, buttons) {
+               if (!nested) {
+                       nested = '';
+                       buttons = this.s.buttons;
+               }
+
+               for (var i = 0, ien = buttons.length; i < ien; i++) {
+                       var inner = buttons[i].buttons;
+
+                       if (buttons[i].node === node) {
+                               return nested + i;
+                       }
+
+                       if (inner && inner.length) {
+                               var match = this.index(node, i + '-', inner);
+
+                               if (match !== null) {
+                                       return match;
+                               }
+                       }
+               }
+
+               return null;
+       },
+
+       /**
+        * Get the instance name for the button set selector
+        * @return {string} Instance name
+        */
+       name: function () {
+               return this.c.name;
+       },
+
+       /**
+        * Get a button's node of the buttons container if no button is given
+        * @param  {node} [node] Button node
+        * @return {jQuery} Button element, or container
+        */
+       node: function (node) {
+               if (!node) {
+                       return this.dom.container;
+               }
+
+               var button = this._nodeToButton(node);
+               return $(button.node);
+       },
+
+       /**
+        * Set / get a processing class on the selected button
+        * @param {element} node Triggering button node
+        * @param  {boolean} flag true to add, false to remove, undefined to get
+        * @return {boolean|Buttons} Getter value or this if a setter.
+        */
+       processing: function (node, flag) {
+               var dt = this.s.dt;
+               var button = this._nodeToButton(node);
+
+               if (flag === undefined) {
+                       return $(button.node).hasClass('processing');
+               }
+
+               $(button.node).toggleClass('processing', flag);
+
+               $(dt.table().node()).triggerHandler('buttons-processing.dt', [
+                       flag,
+                       dt.button(node),
+                       dt,
+                       $(node),
+                       button.conf
+               ]);
+
+               return this;
+       },
+
+       /**
+        * Remove a button.
+        * @param  {node} node Button node
+        * @return {Buttons} Self for chaining
+        */
+       remove: function (node) {
+               var button = this._nodeToButton(node);
+               var host = this._nodeToHost(node);
+               var dt = this.s.dt;
+
+               // Remove any child buttons first
+               if (button.buttons.length) {
+                       for (var i = button.buttons.length - 1; i >= 0; i--) {
+                               this.remove(button.buttons[i].node);
+                       }
+               }
+
+               button.conf.destroying = true;
+
+               // Allow the button to remove event handlers, etc
+               if (button.conf.destroy) {
+                       button.conf.destroy.call(dt.button(node), dt, $(node), 
button.conf);
+               }
+
+               this._removeKey(button.conf);
+
+               $(button.node).remove();
+
+               var idx = $.inArray(button, host);
+               host.splice(idx, 1);
+
+               return this;
+       },
+
+       /**
+        * Get the text for a button
+        * @param  {int|string} node Button index
+        * @return {string} Button text
+        */ /**
+        * Set the text for a button
+        * @param  {int|string|function} node Button index
+        * @param  {string} label Text
+        * @return {Buttons} Self for chaining
+        */
+       text: function (node, label) {
+               var button = this._nodeToButton(node);
+               var textNode = button.textNode;
+               var dt = this.s.dt;
+               var jqNode = $(button.node);
+               var text = function (opt) {
+                       return typeof opt === 'function' ? opt(dt, jqNode, 
button.conf) : opt;
+               };
+
+               if (label === undefined) {
+                       return text(button.conf.text);
+               }
+
+               button.conf.text = label;
+               textNode.html(text(label));
+
+               return this;
+       },
+
+       /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 
* * *
+        * Constructor
+        */
+
+       /**
+        * Buttons constructor
+        * @private
+        */
+       _constructor: function () {
+               var that = this;
+               var dt = this.s.dt;
+               var dtSettings = dt.settings()[0];
+               var buttons = this.c.buttons;
+
+               if (!dtSettings._buttons) {
+                       dtSettings._buttons = [];
+               }
+
+               dtSettings._buttons.push({
+                       inst: this,
+                       name: this.c.name
+               });
+
+               for (var i = 0, ien = buttons.length; i < ien; i++) {
+                       this.add(buttons[i]);
+               }
+
+               dt.on('destroy', function (e, settings) {
+                       if (settings === dtSettings) {
+                               that.destroy();
+                       }
+               });
+
+               // Global key event binding to listen for button keys
+               $('body').on('keyup.' + this.s.namespace, function (e) {
+                       if (!document.activeElement || document.activeElement 
=== document.body) {
+                               // SUse a string of characters for fast lookup 
of if we need to
+                               // handle this
+                               var character = 
String.fromCharCode(e.keyCode).toLowerCase();
+
+                               if 
(that.s.listenKeys.toLowerCase().indexOf(character) !== -1) {
+                                       that._keypress(character, e);
+                               }
+                       }
+               });
+       },
+
+       /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 
* * *
+        * Private methods
+        */
+
+       /**
+        * Add a new button to the key press listener
+        * @param {object} conf Resolved button configuration object
+        * @private
+        */
+       _addKey: function (conf) {
+               if (conf.key) {
+                       this.s.listenKeys += $.isPlainObject(conf.key) ? 
conf.key.key : conf.key;
+               }
+       },
+
+       /**
+        * Insert the buttons into the container. Call without parameters!
+        * @param  {node} [container] Recursive only - Insert point
+        * @param  {array} [buttons] Recursive only - Buttons array
+        * @private
+        */
+       _draw: function (container, buttons) {
+               if (!container) {
+                       container = this.dom.container;
+                       buttons = this.s.buttons;
+               }
+
+               container.children().detach();
+
+               for (var i = 0, ien = buttons.length; i < ien; i++) {
+                       container.append(buttons[i].inserter);
+                       container.append(' ');
+
+                       if (buttons[i].buttons && buttons[i].buttons.length) {
+                               this._draw(buttons[i].collection, 
buttons[i].buttons);
+                       }
+               }
+       },
+
+       /**
+        * Create buttons from an array of buttons
+        * @param  {array} attachTo Buttons array to attach to
+        * @param  {object} button Button definition
+        * @param  {boolean} inCollection true if the button is in a collection
+        * @private
+        */
+       _expandButton: function (
+               attachTo,
+               button,
+               split,
+               inCollection,
+               inSplit,
+               attachPoint,
+               parentConf
+       ) {
+               var dt = this.s.dt;
+               var isSplit = false;
+               var domCollection = this.c.dom.collection;
+               var buttons = !Array.isArray(button) ? [button] : button;
+
+               if (button === undefined) {
+                       buttons = !Array.isArray(split) ? [split] : split;
+               }
+
+               for (var i = 0, ien = buttons.length; i < ien; i++) {
+                       var conf = this._resolveExtends(buttons[i]);
+
+                       if (!conf) {
+                               continue;
+                       }
+
+                       isSplit = conf.config && conf.config.split ? true : 
false;
+
+                       // If the configuration is an array, then expand the 
buttons at this
+                       // point
+                       if (Array.isArray(conf)) {
+                               this._expandButton(
+                                       attachTo,
+                                       conf,
+                                       built !== undefined && built.conf !== 
undefined ? built.conf.split : undefined,
+                                       inCollection,
+                                       parentConf !== undefined && 
parentConf.split !== undefined,
+                                       attachPoint,
+                                       parentConf
+                               );
+                               continue;
+                       }
+
+                       var built = this._buildButton(
+                               conf,
+                               inCollection,
+                               conf.split !== undefined ||
+                                       (conf.config !== undefined && 
conf.config.split !== undefined),
+                               inSplit
+                       );
+                       if (!built) {
+                               continue;
+                       }
+
+                       if (attachPoint !== undefined && attachPoint !== null) {
+                               attachTo.splice(attachPoint, 0, built);
+                               attachPoint++;
+                       }
+                       else {
+                               attachTo.push(built);
+                       }
+
+                       // Create the dropdown for a collection
+                       if (built.conf.buttons) {
+                               built.collection = $('<' + 
domCollection.container.content.tag + '/>');
+                               built.conf._collection = built.collection;
+
+                               
$(built.node).append(domCollection.action.dropHtml);
+
+                               this._expandButton(
+                                       built.buttons,
+                                       built.conf.buttons,
+                                       built.conf.split,
+                                       !isSplit,
+                                       isSplit,
+                                       attachPoint,
+                                       built.conf
+                               );
+                       }
+
+                       // And the split collection
+                       if (built.conf.split) {
+                               built.collection = $('<' + 
domCollection.container.tag + '/>');
+                               built.conf._collection = built.collection;
+
+                               for (var j = 0; j < built.conf.split.length; 
j++) {
+                                       var item = built.conf.split[j];
+
+                                       if (typeof item === 'object') {
+                                               item.parent = parentConf;
+
+                                               if (item.collectionLayout === 
undefined) {
+                                                       item.collectionLayout = 
built.conf.collectionLayout;
+                                               }
+
+                                               if (item.dropup === undefined) {
+                                                       item.dropup = 
built.conf.dropup;
+                                               }
+
+                                               if (item.fade === undefined) {
+                                                       item.fade = 
built.conf.fade;
+                                               }
+                                       }
+                               }
+
+                               this._expandButton(
+                                       built.buttons,
+                                       built.conf.buttons,
+                                       built.conf.split,
+                                       !isSplit,
+                                       isSplit,
+                                       attachPoint,
+                                       built.conf
+                               );
+                       }
+
+                       built.conf.parent = parentConf;
+
+                       // init call is made here, rather than buildButton as 
it needs to
+                       // be selectable, and for that it needs to be in the 
buttons array
+                       if (conf.init) {
+                               conf.init.call(dt.button(built.node), dt, 
$(built.node), conf);
+                       }
+               }
+       },
+
+       /**
+        * Create an individual button
+        * @param  {object} config            Resolved button configuration
+        * @param  {boolean} inCollection `true` if a collection button
+        * @return {object} Completed button description object
+        * @private
+        */
+       _buildButton: function (config, inCollection, isSplit, inSplit) {
+               var configDom = this.c.dom;
+               var textNode;
+               var dt = this.s.dt;
+               var text = function (opt) {
+                       return typeof opt === 'function' ? opt(dt, button, 
config) : opt;
+               };
+
+               // Create an object that describes the button which can be in 
`dom.button`, or
+               // `dom.collection.button` or `dom.split.button` or 
`dom.collection.split.button`!
+               // Each should extend from `dom.button`.
+               var dom = $.extend(true, {}, configDom.button);
+
+               if (inCollection && isSplit && configDom.collection.split) {
+                       $.extend(true, dom, configDom.collection.split.action);
+               }
+               else if (inSplit || inCollection) {
+                       $.extend(true, dom, configDom.collection.button);
+               }
+               else if (isSplit) {
+                       $.extend(true, dom, configDom.split.button);
+               }
+
+               // Spacers don't do much other than insert an element into the 
DOM
+               if (config.spacer) {
+                       var spacer = $('<' + dom.spacer.tag + '/>')
+                               .addClass('dt-button-spacer ' + config.style + 
' ' + dom.spacer.className)
+                               .html(text(config.text));
+
+                       return {
+                               conf: config,
+                               node: spacer,
+                               inserter: spacer,
+                               buttons: [],
+                               inCollection: inCollection,
+                               isSplit: isSplit,
+                               collection: null,
+                               textNode: spacer
+                       };
+               }
+
+               // Make sure that the button is available based on whatever 
requirements
+               // it has. For example, PDF button require pdfmake
+               if (config.available && !config.available(dt, config) && 
!config.hasOwnProperty('html')) {
+                       return false;
+               }
+
+               var button;
+
+               if (!config.hasOwnProperty('html')) {
+                       var action = function (e, dt, button, config) {
+                               config.action.call(dt.button(button), e, dt, 
button, config);
+
+                               
$(dt.table().node()).triggerHandler('buttons-action.dt', [
+                                       dt.button(button),
+                                       dt,
+                                       button,
+                                       config
+                               ]);
+                       };
+
+                       var tag = config.tag || dom.tag;
+                       var clickBlurs = config.clickBlurs === undefined ? true 
: config.clickBlurs;
+
+                       button = $('<' + tag + '/>')
+                               .addClass(dom.className)
+                               .attr('tabindex', 
this.s.dt.settings()[0].iTabIndex)
+                               .attr('aria-controls', 
this.s.dt.table().node().id)
+                               .on('click.dtb', function (e) {
+                                       e.preventDefault();
+
+                                       if (!button.hasClass(dom.disabled) && 
config.action) {
+                                               action(e, dt, button, config);
+                                       }
+
+                                       if (clickBlurs) {
+                                               button.trigger('blur');
+                                       }
+                               })
+                               .on('keypress.dtb', function (e) {
+                                       if (e.keyCode === 13) {
+                                               e.preventDefault();
+
+                                               if 
(!button.hasClass(dom.disabled) && config.action) {
+                                                       action(e, dt, button, 
config);
+                                               }
+                                       }
+                               });
+
+                       // Make `a` tags act like a link
+                       if (tag.toLowerCase() === 'a') {
+                               button.attr('href', '#');
+                       }
+
+                       // Button tags should have `type=button` so they don't 
have any default behaviour
+                       if (tag.toLowerCase() === 'button') {
+                               button.attr('type', 'button');
+                       }
+
+                       if (dom.liner.tag) {
+                               var liner = $('<' + dom.liner.tag + '/>')
+                                       .html(text(config.text))
+                                       .addClass(dom.liner.className);
+
+                               if (dom.liner.tag.toLowerCase() === 'a') {
+                                       liner.attr('href', '#');
+                               }
+
+                               button.append(liner);
+                               textNode = liner;
+                       }
+                       else {
+                               button.html(text(config.text));
+                               textNode = button;
+                       }
+
+                       if (config.enabled === false) {
+                               button.addClass(dom.disabled);
+                       }
+
+                       if (config.className) {
+                               button.addClass(config.className);
+                       }
+
+                       if (config.titleAttr) {
+                               button.attr('title', text(config.titleAttr));
+                       }
+
+                       if (config.attr) {
+                               button.attr(config.attr);
+                       }
+
+                       if (!config.namespace) {
+                               config.namespace = '.dt-button-' + 
_buttonCounter++;
+                       }
+
+                       if (config.config !== undefined && config.config.split) 
{
+                               config.split = config.config.split;
+                       }
+               }
+               else {
+                       button = $(config.html);
+               }
+
+               var buttonContainer = this.c.dom.buttonContainer;
+               var inserter;
+               if (buttonContainer && buttonContainer.tag) {
+                       inserter = $('<' + buttonContainer.tag + '/>')
+                               .addClass(buttonContainer.className)
+                               .append(button);
+               }
+               else {
+                       inserter = button;
+               }
+
+               this._addKey(config);
+
+               // Style integration callback for DOM manipulation
+               // Note that this is _not_ documented. It is currently
+               // for style integration only
+               if (this.c.buttonCreated) {
+                       inserter = this.c.buttonCreated(config, inserter);
+               }
+
+               var splitDiv;
+
+               if (isSplit) {
+                       var dropdownConf = inCollection
+                               ? $.extend(true, this.c.dom.split, 
this.c.dom.collection.split)
+                               : this.c.dom.split;
+                       var wrapperConf = dropdownConf.wrapper;
+
+                       splitDiv = $('<' + wrapperConf.tag + '/>')
+                               .addClass(wrapperConf.className)
+                               .append(button);
+
+                       var dropButtonConfig = $.extend(config, {
+                               align: dropdownConf.dropdown.align,
+                               attr: {
+                                       'aria-haspopup': 'dialog',
+                                       'aria-expanded': false
+                               },
+                               className: dropdownConf.dropdown.className,
+                               closeButton: false,
+                               splitAlignClass: 
dropdownConf.dropdown.splitAlignClass,
+                               text: dropdownConf.dropdown.text
+                       });
+
+                       this._addKey(dropButtonConfig);
+
+                       var splitAction = function (e, dt, button, config) {
+                               
_dtButtons.split.action.call(dt.button(splitDiv), e, dt, button, config);
+
+                               
$(dt.table().node()).triggerHandler('buttons-action.dt', [
+                                       dt.button(button),
+                                       dt,
+                                       button,
+                                       config
+                               ]);
+                               button.attr('aria-expanded', true);
+                       };
+
+                       var dropButton = $(
+                               '<button class="' + 
dropdownConf.dropdown.className + ' dt-button"></button>'
+                       )
+                               .html(dropdownConf.dropdown.dropHtml)
+                               .on('click.dtb', function (e) {
+                                       e.preventDefault();
+                                       e.stopPropagation();
+
+                                       if (!dropButton.hasClass(dom.disabled)) 
{
+                                               splitAction(e, dt, dropButton, 
dropButtonConfig);
+                                       }
+                                       if (clickBlurs) {
+                                               dropButton.trigger('blur');
+                                       }
+                               })
+                               .on('keypress.dtb', function (e) {
+                                       if (e.keyCode === 13) {
+                                               e.preventDefault();
+
+                                               if 
(!dropButton.hasClass(dom.disabled)) {
+                                                       splitAction(e, dt, 
dropButton, dropButtonConfig);
+                                               }
+                                       }
+                               });
+
+                       if (config.split.length === 0) {
+                               dropButton.addClass('dtb-hide-drop');
+                       }
+
+                       splitDiv.append(dropButton).attr(dropButtonConfig.attr);
+               }
+
+               return {
+                       conf: config,
+                       node: isSplit ? splitDiv.get(0) : button.get(0),
+                       inserter: isSplit ? splitDiv : inserter,
+                       buttons: [],
+                       inCollection: inCollection,
+                       isSplit: isSplit,
+                       inSplit: inSplit,
+                       collection: null,
+                       textNode: textNode
+               };
+       },
+
+       /**
+        * Get the button object from a node (recursive)
+        * @param  {node} node Button node
+        * @param  {array} [buttons] Button array, uses base if not defined
+        * @return {object} Button object
+        * @private
+        */
+       _nodeToButton: function (node, buttons) {
+               if (!buttons) {
+                       buttons = this.s.buttons;
+               }
+
+               for (var i = 0, ien = buttons.length; i < ien; i++) {
+                       if (buttons[i].node === node) {
+                               return buttons[i];
+                       }
+
+                       if (buttons[i].buttons.length) {
+                               var ret = this._nodeToButton(node, 
buttons[i].buttons);
+
+                               if (ret) {
+                                       return ret;
+                               }
+                       }
+               }
+       },
+
+       /**
+        * Get container array for a button from a button node (recursive)
+        * @param  {node} node Button node
+        * @param  {array} [buttons] Button array, uses base if not defined
+        * @return {array} Button's host array
+        * @private
+        */
+       _nodeToHost: function (node, buttons) {
+               if (!buttons) {
+                       buttons = this.s.buttons;
+               }
+
+               for (var i = 0, ien = buttons.length; i < ien; i++) {
+                       if (buttons[i].node === node) {
+                               return buttons;
+                       }
+
+                       if (buttons[i].buttons.length) {
+                               var ret = this._nodeToHost(node, 
buttons[i].buttons);
+
+                               if (ret) {
+                                       return ret;
+                               }
+                       }
+               }
+       },
+
+       /**
+        * Handle a key press - determine if any button's key configured matches
+        * what was typed and trigger the action if so.
+        * @param  {string} character The character pressed
+        * @param  {object} e Key event that triggered this call
+        * @private
+        */
+       _keypress: function (character, e) {
+               // Check if this button press already activated on another 
instance of Buttons
+               if (e._buttonsHandled) {
+                       return;
+               }
+
+               var run = function (conf, node) {
+                       if (!conf.key) {
+                               return;
+                       }
+
+                       if (conf.key === character) {
+                               e._buttonsHandled = true;
+                               $(node).click();
+                       }
+                       else if ($.isPlainObject(conf.key)) {
+                               if (conf.key.key !== character) {
+                                       return;
+                               }
+
+                               if (conf.key.shiftKey && !e.shiftKey) {
+                                       return;
+                               }
+
+                               if (conf.key.altKey && !e.altKey) {
+                                       return;
+                               }
+
+                               if (conf.key.ctrlKey && !e.ctrlKey) {
+                                       return;
+                               }
+
+                               if (conf.key.metaKey && !e.metaKey) {
+                                       return;
+                               }
+
+                               // Made it this far - it is good
+                               e._buttonsHandled = true;
+                               $(node).click();
+                       }
+               };
+
+               var recurse = function (a) {
+                       for (var i = 0, ien = a.length; i < ien; i++) {
+                               run(a[i].conf, a[i].node);
+
+                               if (a[i].buttons.length) {
+                                       recurse(a[i].buttons);
+                               }
+                       }
+               };
+
+               recurse(this.s.buttons);
+       },
+
+       /**
+        * Remove a key from the key listener for this instance (to be used 
when a
+        * button is removed)
+        * @param  {object} conf Button configuration
+        * @private
+        */
+       _removeKey: function (conf) {
+               if (conf.key) {
+                       var character = $.isPlainObject(conf.key) ? 
conf.key.key : conf.key;
+
+                       // Remove only one character, as multiple buttons could 
have the
+                       // same listening key
+                       var a = this.s.listenKeys.split('');
+                       var idx = $.inArray(character, a);
+                       a.splice(idx, 1);
+                       this.s.listenKeys = a.join('');
+               }
+       },
+
+       /**
+        * Resolve a button configuration
+        * @param  {string|function|object} conf Button config to resolve
+        * @return {object} Button configuration
+        * @private
+        */
+       _resolveExtends: function (conf) {
+               var that = this;
+               var dt = this.s.dt;
+               var i, ien;
+               var toConfObject = function (base) {
+                       var loop = 0;
+
+                       // Loop until we have resolved to a button 
configuration, or an
+                       // array of button configurations (which will be 
iterated
+                       // separately)
+                       while (!$.isPlainObject(base) && !Array.isArray(base)) {
+                               if (base === undefined) {
+                                       return;
+                               }
+
+                               if (typeof base === 'function') {
+                                       base = base.call(that, dt, conf);
+
+                                       if (!base) {
+                                               return false;
+                                       }
+                               }
+                               else if (typeof base === 'string') {
+                                       if (!_dtButtons[base]) {
+                                               return { html: base };
+                                       }
+
+                                       base = _dtButtons[base];
+                               }
+
+                               loop++;
+                               if (loop > 30) {
+                                       // Protect against misconfiguration 
killing the browser
+                                       throw 'Buttons: Too many iterations';
+                               }
+                       }
+
+                       return Array.isArray(base) ? base : $.extend({}, base);
+               };
+
+               conf = toConfObject(conf);
+
+               while (conf && conf.extend) {
+                       // Use `toConfObject` in case the button definition 
being extended
+                       // is itself a string or a function
+                       if (!_dtButtons[conf.extend]) {
+                               throw 'Cannot extend unknown button type: ' + 
conf.extend;
+                       }
+
+                       var objArray = toConfObject(_dtButtons[conf.extend]);
+                       if (Array.isArray(objArray)) {
+                               return objArray;
+                       }
+                       else if (!objArray) {
+                               // This is a little brutal as it might be 
possible to have a
+                               // valid button without the extend, but if 
there is no extend
+                               // then the host button would be acting in an 
undefined state
+                               return false;
+                       }
+
+                       // Stash the current class name
+                       var originalClassName = objArray.className;
+
+                       if (conf.config !== undefined && objArray.config !== 
undefined) {
+                               conf.config = $.extend({}, objArray.config, 
conf.config);
+                       }
+
+                       conf = $.extend({}, objArray, conf);
+
+                       // The extend will have overwritten the original class 
name if the
+                       // `conf` object also assigned a class, but we want to 
concatenate
+                       // them so they are list that is combined from all 
extended buttons
+                       if (originalClassName && conf.className !== 
originalClassName) {
+                               conf.className = originalClassName + ' ' + 
conf.className;
+                       }
+
+                       // Although we want the `conf` object to overwrite 
almost all of
+                       // the properties of the object being extended, the 
`extend`
+                       // property should come from the object being extended
+                       conf.extend = objArray.extend;
+               }
+
+               // Buttons to be added to a collection  -gives the ability to 
define
+               // if buttons should be added to the start or end of a 
collection
+               var postfixButtons = conf.postfixButtons;
+               if (postfixButtons) {
+                       if (!conf.buttons) {
+                               conf.buttons = [];
+                       }
+
+                       for (i = 0, ien = postfixButtons.length; i < ien; i++) {
+                               conf.buttons.push(postfixButtons[i]);
+                       }
+               }
+
+               var prefixButtons = conf.prefixButtons;
+               if (prefixButtons) {
+                       if (!conf.buttons) {
+                               conf.buttons = [];
+                       }
+
+                       for (i = 0, ien = prefixButtons.length; i < ien; i++) {
+                               conf.buttons.splice(i, 0, prefixButtons[i]);
+                       }
+               }
+
+               return conf;
+       },
+
+       /**
+        * Display (and replace if there is an existing one) a popover attached 
to a button
+        * @param {string|node} content Content to show
+        * @param {DataTable.Api} hostButton DT API instance of the button
+        * @param {object} inOpts Options (see object below for all options)
+        */
+       _popover: function (content, hostButton, inOpts, e) {
+               var dt = hostButton;
+               var c = this.c;
+               var closed = false;
+               var options = $.extend(
+                       {
+                               align: 'button-left', // button-right, 
dt-container, split-left, split-right
+                               autoClose: false,
+                               background: true,
+                               backgroundClassName: 'dt-button-background',
+                               closeButton: true,
+                               containerClassName: 
c.dom.collection.container.className,
+                               contentClassName: 
c.dom.collection.container.content.className,
+                               collectionLayout: '',
+                               collectionTitle: '',
+                               dropup: false,
+                               fade: 400,
+                               popoverTitle: '',
+                               rightAlignClassName: 'dt-button-right',
+                               tag: c.dom.collection.container.tag
+                       },
+                       inOpts
+               );
+
+               var containerSelector = options.tag + '.' + 
options.containerClassName.replace(/ /g, '.');
+               var hostNode = hostButton.node();
+
+               var close = function () {
+                       closed = true;
+
+                       _fadeOut($(containerSelector), options.fade, function 
() {
+                               $(this).detach();
+                       });
+
+                       
$(dt.buttons('[aria-haspopup="dialog"][aria-expanded="true"]').nodes()).attr(
+                               'aria-expanded',
+                               'false'
+                       );
+
+                       
$('div.dt-button-background').off('click.dtb-collection');
+                       Buttons.background(false, options.backgroundClassName, 
options.fade, hostNode);
+
+                       $(window).off('resize.resize.dtb-collection');
+                       $('body').off('.dtb-collection');
+                       dt.off('buttons-action.b-internal');
+                       dt.off('destroy');
+               };
+
+               if (content === false) {
+                       close();
+                       return;
+               }
+
+               var existingExpanded = $(
+                       
dt.buttons('[aria-haspopup="dialog"][aria-expanded="true"]').nodes()
+               );
+               if (existingExpanded.length) {
+                       // Reuse the current position if the button that was 
triggered is inside an existing collection
+                       if (hostNode.closest(containerSelector).length) {
+                               hostNode = existingExpanded.eq(0);
+                       }
+
+                       close();
+               }
+
+               // Try to be smart about the layout
+               var cnt = $('.dt-button', content).length;
+               var mod = '';
+
+               if (cnt === 3) {
+                       mod = 'dtb-b3';
+               }
+               else if (cnt === 2) {
+                       mod = 'dtb-b2';
+               }
+               else if (cnt === 1) {
+                       mod = 'dtb-b1';
+               }
+
+               var display = $('<' + options.tag + '/>')
+                       .addClass(options.containerClassName)
+                       .addClass(options.collectionLayout)
+                       .addClass(options.splitAlignClass)
+                       .addClass(mod)
+                       .css('display', 'none')
+                       .attr({
+                               'aria-modal': true,
+                               role: 'dialog'
+                       });
+
+               content = $(content)
+                       .addClass(options.contentClassName)
+                       .attr('role', 'menu')
+                       .appendTo(display);
+
+               hostNode.attr('aria-expanded', 'true');
+
+               if (hostNode.parents('body')[0] !== document.body) {
+                       hostNode = document.body.lastChild;
+               }
+
+               if (options.popoverTitle) {
+                       display.prepend(
+                               '<div class="dt-button-collection-title">' + 
options.popoverTitle + '</div>'
+                       );
+               }
+               else if (options.collectionTitle) {
+                       display.prepend(
+                               '<div class="dt-button-collection-title">' + 
options.collectionTitle + '</div>'
+                       );
+               }
+
+               if (options.closeButton) {
+                       display
+                               .prepend('<div 
class="dtb-popover-close">&times;</div>')
+                               .addClass('dtb-collection-closeable');
+               }
+
+               _fadeIn(display.insertAfter(hostNode), options.fade);
+
+               var tableContainer = $(hostButton.table().container());
+               var position = display.css('position');
+
+               if (options.span === 'container' || options.align === 
'dt-container') {
+                       hostNode = hostNode.parent();
+                       display.css('width', tableContainer.width());
+               }
+
+               // Align the popover relative to the DataTables container
+               // Useful for wide popovers such as SearchPanes
+               if (position === 'absolute') {
+                       // Align relative to the host button
+                       var offsetParent = $(hostNode[0].offsetParent);
+                       var buttonPosition = hostNode.position();
+                       var buttonOffset = hostNode.offset();
+                       var tableSizes = offsetParent.offset();
+                       var containerPosition = offsetParent.position();
+                       var computed = window.getComputedStyle(offsetParent[0]);
+
+                       tableSizes.height = offsetParent.outerHeight();
+                       tableSizes.width = offsetParent.width() + 
parseFloat(computed.paddingLeft);
+                       tableSizes.right = tableSizes.left + tableSizes.width;
+                       tableSizes.bottom = tableSizes.top + tableSizes.height;
+
+                       // Set the initial position so we can read height / 
width
+                       var top = buttonPosition.top + hostNode.outerHeight();
+                       var left = buttonPosition.left;
+
+                       display.css({
+                               top: top,
+                               left: left
+                       });
+
+                       // Get the popover position
+                       computed = window.getComputedStyle(display[0]);
+                       var popoverSizes = display.offset();
+
+                       popoverSizes.height = display.outerHeight();
+                       popoverSizes.width = display.outerWidth();
+                       popoverSizes.right = popoverSizes.left + 
popoverSizes.width;
+                       popoverSizes.bottom = popoverSizes.top + 
popoverSizes.height;
+                       popoverSizes.marginTop = parseFloat(computed.marginTop);
+                       popoverSizes.marginBottom = 
parseFloat(computed.marginBottom);
+
+                       // First position per the class requirements - pop up 
and right align
+                       if (options.dropup) {
+                               top =
+                                       buttonPosition.top -
+                                       popoverSizes.height -
+                                       popoverSizes.marginTop -
+                                       popoverSizes.marginBottom;
+                       }
+
+                       if (options.align === 'button-right' || 
display.hasClass(options.rightAlignClassName)) {
+                               left = buttonPosition.left - popoverSizes.width 
+ hostNode.outerWidth();
+                       }
+
+                       // Container alignment - make sure it doesn't overflow 
the table container
+                       if (options.align === 'dt-container' || options.align 
=== 'container') {
+                               if (left < buttonPosition.left) {
+                                       left = -buttonPosition.left;
+                               }
+
+                               if (left + popoverSizes.width > 
tableSizes.width) {
+                                       left = tableSizes.width - 
popoverSizes.width;
+                               }
+                       }
+
+                       // Window adjustment
+                       if (containerPosition.left + left + popoverSizes.width 
> $(window).width()) {
+                               // Overflowing the document to the right
+                               left = $(window).width() - popoverSizes.width - 
containerPosition.left;
+                       }
+
+                       if (buttonOffset.left + left < 0) {
+                               // Off to the left of the document
+                               left = -buttonOffset.left;
+                       }
+
+                       if (
+                               containerPosition.top + top + 
popoverSizes.height >
+                               $(window).height() + $(window).scrollTop()
+                       ) {
+                               // Pop up if otherwise we'd need the user to 
scroll down
+                               top =
+                                       buttonPosition.top -
+                                       popoverSizes.height -
+                                       popoverSizes.marginTop -
+                                       popoverSizes.marginBottom;
+                       }
+
+                       if (containerPosition.top + top < 
$(window).scrollTop()) {
+                               // Correction for when the top is beyond the 
top of the page
+                               top = buttonPosition.top + 
hostNode.outerHeight();
+                       }
+
+                       // Calculations all done - now set it
+                       display.css({
+                               top: top,
+                               left: left
+                       });
+               }
+               else {
+                       // Fix position - centre on screen
+                       var position = function () {
+                               var half = $(window).height() / 2;
+
+                               var top = display.height() / 2;
+                               if (top > half) {
+                                       top = half;
+                               }
+
+                               display.css('marginTop', top * -1);
+                       };
+
+                       position();
+
+                       $(window).on('resize.dtb-collection', function () {
+                               position();
+                       });
+               }
+
+               if (options.background) {
+                       Buttons.background(
+                               true,
+                               options.backgroundClassName,
+                               options.fade,
+                               options.backgroundHost || hostNode
+                       );
+               }
+
+               // This is bonkers, but if we don't have a click listener on the
+               // background element, iOS Safari will ignore the body click
+               // listener below. An empty function here is all that is
+               // required to make it work...
+               $('div.dt-button-background').on('click.dtb-collection', 
function () {});
+
+               if (options.autoClose) {
+                       setTimeout(function () {
+                               dt.on('buttons-action.b-internal', function (e, 
btn, dt, node) {
+                                       if (node[0] === hostNode[0]) {
+                                               return;
+                                       }
+                                       close();
+                               });
+                       }, 0);
+               }
+
+               $(display).trigger('buttons-popover.dt');
+
+               dt.on('destroy', close);
+
+               setTimeout(function () {
+                       closed = false;
+                       $('body')
+                               .on('click.dtb-collection', function (e) {
+                                       if (closed) {
+                                               return;
+                                       }
+
+                                       // andSelf is deprecated in jQ1.8, but 
we want 1.7 compat
+                                       var back = $.fn.addBack ? 'addBack' : 
'andSelf';
+                                       var parent = $(e.target).parent()[0];
+
+                                       if (
+                                               
(!$(e.target).parents()[back]().filter(content).length &&
+                                                       
!$(parent).hasClass('dt-buttons')) ||
+                                               
$(e.target).hasClass('dt-button-background')
+                                       ) {
+                                               close();
+                                       }
+                               })
+                               .on('keyup.dtb-collection', function (e) {
+                                       if (e.keyCode === 27) {
+                                               close();
+                                       }
+                               })
+                               .on('keydown.dtb-collection', function (e) {
+                                       // Focus trap for tab key
+                                       var elements = $('a, button', content);
+                                       var active = document.activeElement;
+
+                                       if (e.keyCode !== 9) {
+                                               // tab
+                                               return;
+                                       }
+
+                                       if (elements.index(active) === -1) {
+                                               // If current focus is not 
inside the popover
+                                               elements.first().focus();
+                                               e.preventDefault();
+                                       }
+                                       else if (e.shiftKey) {
+                                               // Reverse tabbing order when 
shift key is pressed
+                                               if (active === elements[0]) {
+                                                       elements.last().focus();
+                                                       e.preventDefault();
+                                               }
+                                       }
+                                       else {
+                                               if (active === 
elements.last()[0]) {
+                                                       
elements.first().focus();
+                                                       e.preventDefault();
+                                               }
+                                       }
+                               });
+               }, 0);
+       }
+});
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * Statics
+ */
+
+/**
+ * Show / hide a background layer behind a collection
+ * @param  {boolean} Flag to indicate if the background should be shown or
+ *   hidden
+ * @param  {string} Class to assign to the background
+ * @static
+ */
+Buttons.background = function (show, className, fade, insertPoint) {
+       if (fade === undefined) {
+               fade = 400;
+       }
+       if (!insertPoint) {
+               insertPoint = document.body;
+       }
+
+       if (show) {
+               _fadeIn(
+                       $('<div/>').addClass(className).css('display', 
'none').insertAfter(insertPoint),
+                       fade
+               );
+       }
+       else {
+               _fadeOut($('div.' + className), fade, function () {
+                       $(this).removeClass(className).remove();
+               });
+       }
+};
+
+/**
+ * Instance selector - select Buttons instances based on an instance selector
+ * value from the buttons assigned to a DataTable. This is only useful if
+ * multiple instances are attached to a DataTable.
+ * @param  {string|int|array} Instance selector - see `instance-selector`
+ *   documentation on the DataTables site
+ * @param  {array} Button instance array that was attached to the DataTables
+ *   settings object
+ * @return {array} Buttons instances
+ * @static
+ */
+Buttons.instanceSelector = function (group, buttons) {
+       if (group === undefined || group === null) {
+               return $.map(buttons, function (v) {
+                       return v.inst;
+               });
+       }
+
+       var ret = [];
+       var names = $.map(buttons, function (v) {
+               return v.name;
+       });
+
+       // Flatten the group selector into an array of single options
+       var process = function (input) {
+               if (Array.isArray(input)) {
+                       for (var i = 0, ien = input.length; i < ien; i++) {
+                               process(input[i]);
+                       }
+                       return;
+               }
+
+               if (typeof input === 'string') {
+                       if (input.indexOf(',') !== -1) {
+                               // String selector, list of names
+                               process(input.split(','));
+                       }
+                       else {
+                               // String selector individual name
+                               var idx = $.inArray(input.trim(), names);
+
+                               if (idx !== -1) {
+                                       ret.push(buttons[idx].inst);
+                               }
+                       }
+               }
+               else if (typeof input === 'number') {
+                       // Index selector
+                       ret.push(buttons[input].inst);
+               }
+               else if (typeof input === 'object') {
+                       // Actual instance selector
+                       ret.push(input);
+               }
+       };
+
+       process(group);
+
+       return ret;
+};
+
+/**
+ * Button selector - select one or more buttons from a selector input so some
+ * operation can be performed on them.
+ * @param  {array} Button instances array that the selector should operate on
+ * @param  {string|int|node|jQuery|array} Button selector - see
+ *   `button-selector` documentation on the DataTables site
+ * @return {array} Array of objects containing `inst` and `idx` properties of
+ *   the selected buttons so you know which instance each button belongs to.
+ * @static
+ */
+Buttons.buttonSelector = function (insts, selector) {
+       var ret = [];
+       var nodeBuilder = function (a, buttons, baseIdx) {
+               var button;
+               var idx;
+
+               for (var i = 0, ien = buttons.length; i < ien; i++) {
+                       button = buttons[i];
+
+                       if (button) {
+                               idx = baseIdx !== undefined ? baseIdx + i : i + 
'';
+
+                               a.push({
+                                       node: button.node,
+                                       name: button.conf.name,
+                                       idx: idx
+                               });
+
+                               if (button.buttons) {
+                                       nodeBuilder(a, button.buttons, idx + 
'-');
+                               }
+                       }
+               }
+       };
+
+       var run = function (selector, inst) {
+               var i, ien;
+               var buttons = [];
+               nodeBuilder(buttons, inst.s.buttons);
+
+               var nodes = $.map(buttons, function (v) {
+                       return v.node;
+               });
+
+               if (Array.isArray(selector) || selector instanceof $) {
+                       for (i = 0, ien = selector.length; i < ien; i++) {
+                               run(selector[i], inst);
+                       }
+                       return;
+               }
+
+               if (selector === null || selector === undefined || selector === 
'*') {
+                       // Select all
+                       for (i = 0, ien = buttons.length; i < ien; i++) {
+                               ret.push({
+                                       inst: inst,
+                                       node: buttons[i].node
+                               });
+                       }
+               }
+               else if (typeof selector === 'number') {
+                       // Main button index selector
+                       if (inst.s.buttons[selector]) {
+                               ret.push({
+                                       inst: inst,
+                                       node: inst.s.buttons[selector].node
+                               });
+                       }
+               }
+               else if (typeof selector === 'string') {
+                       if (selector.indexOf(',') !== -1) {
+                               // Split
+                               var a = selector.split(',');
+
+                               for (i = 0, ien = a.length; i < ien; i++) {
+                                       run(a[i].trim(), inst);
+                               }
+                       }
+                       else if (selector.match(/^\d+(\-\d+)*$/)) {
+                               // Sub-button index selector
+                               var indexes = $.map(buttons, function (v) {
+                                       return v.idx;
+                               });
+
+                               ret.push({
+                                       inst: inst,
+                                       node: buttons[$.inArray(selector, 
indexes)].node
+                               });
+                       }
+                       else if (selector.indexOf(':name') !== -1) {
+                               // Button name selector
+                               var name = selector.replace(':name', '');
+
+                               for (i = 0, ien = buttons.length; i < ien; i++) 
{
+                                       if (buttons[i].name === name) {
+                                               ret.push({
+                                                       inst: inst,
+                                                       node: buttons[i].node
+                                               });
+                                       }
+                               }
+                       }
+                       else {
+                               // jQuery selector on the nodes
+                               $(nodes)
+                                       .filter(selector)
+                                       .each(function () {
+                                               ret.push({
+                                                       inst: inst,
+                                                       node: this
+                                               });
+                                       });
+                       }
+               }
+               else if (typeof selector === 'object' && selector.nodeName) {
+                       // Node selector
+                       var idx = $.inArray(selector, nodes);
+
+                       if (idx !== -1) {
+                               ret.push({
+                                       inst: inst,
+                                       node: nodes[idx]
+                               });
+                       }
+               }
+       };
+
+       for (var i = 0, ien = insts.length; i < ien; i++) {
+               var inst = insts[i];
+
+               run(selector, inst);
+       }
+
+       return ret;
+};
+
+/**
+ * Default function used for formatting output data.
+ * @param {*} str Data to strip
+ */
+Buttons.stripData = function (str, config) {
+       if (typeof str !== 'string') {
+               return str;
+       }
+
+       // Always remove script tags
+       str = 
str.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
+
+       // Always remove comments
+       str = str.replace(/<!\-\-.*?\-\->/g, '');
+
+       if (!config || config.stripHtml) {
+               str = str.replace(/<[^>]*>/g, '');
+       }
+
+       if (!config || config.trim) {
+               str = str.replace(/^\s+|\s+$/g, '');
+       }
+
+       if (!config || config.stripNewlines) {
+               str = str.replace(/\n/g, ' ');
+       }
+
+       if (!config || config.decodeEntities) {
+               if (_entityDecoder) {
+                       str = _entityDecoder(str);
+               }
+               else {
+                       _exportTextarea.innerHTML = str;
+                       str = _exportTextarea.value;
+               }
+       }
+
+       return str;
+};
+
+/**
+ * Provide a custom entity decoding function - e.g. a regex one, which can be
+ * much faster than the built in DOM option, but also larger code size.
+ * @param {function} fn
+ */
+Buttons.entityDecoder = function (fn) {
+       _entityDecoder = fn;
+}
+
+/**
+ * Buttons defaults. For full documentation, please refer to the docs/option
+ * directory or the DataTables site.
+ * @type {Object}
+ * @static
+ */
+Buttons.defaults = {
+       buttons: ['copy', 'excel', 'csv', 'pdf', 'print'],
+       name: 'main',
+       tabIndex: 0,
+       dom: {
+               container: {
+                       tag: 'div',
+                       className: 'dt-buttons'
+               },
+               collection: {
+                       action: {
+                               // action button
+                               dropHtml: '<span 
class="dt-button-down-arrow">&#x25BC;</span>'
+                       },
+                       container: {
+                               // The element used for the dropdown
+                               className: 'dt-button-collection',
+                               content: {
+                                       className: '',
+                                       tag: 'div'
+                               },
+                               tag: 'div'
+                       }
+                       // optionally
+                       // , button: IButton - buttons inside the collection 
container
+                       // , split: ISplit - splits inside the collection 
container
+               },
+               button: {
+                       tag: 'button',
+                       className: 'dt-button',
+                       active: 'dt-button-active', // class name
+                       disabled: 'disabled', // class name
+                       spacer: {
+                               className: 'dt-button-spacer',
+                               tag: 'span'
+                       },
+                       liner: {
+                               tag: 'span',
+                               className: ''
+                       }
+               },
+               split: {
+                       action: {
+                               // action button
+                               className: 'dt-button-split-drop-button 
dt-button',
+                               tag: 'button'
+                       },
+                       dropdown: {
+                               // button to trigger the dropdown
+                               align: 'split-right',
+                               className: 'dt-button-split-drop',
+                               dropHtml: '<span 
class="dt-button-down-arrow">&#x25BC;</span>',
+                               splitAlignClass: 'dt-button-split-left',
+                               tag: 'button'
+                       },
+                       wrapper: {
+                               // wrap around both
+                               className: 'dt-button-split',
+                               tag: 'div'
+                       }
+               }
+       }
+};
+
+/**
+ * Version information
+ * @type {string}
+ * @static
+ */
+Buttons.version = '2.4.2';
+
+$.extend(_dtButtons, {
+       collection: {
+               text: function (dt) {
+                       return dt.i18n('buttons.collection', 'Collection');
+               },
+               className: 'buttons-collection',
+               closeButton: false,
+               init: function (dt, button, config) {
+                       button.attr('aria-expanded', false);
+               },
+               action: function (e, dt, button, config) {
+                       if (config._collection.parents('body').length) {
+                               this.popover(false, config);
+                       }
+                       else {
+                               this.popover(config._collection, config);
+                       }
+
+                       // When activated using a key - auto focus on the
+                       // first item in the popover
+                       if (e.type === 'keypress') {
+                               $('a, button', 
config._collection).eq(0).focus();
+                       }
+               },
+               attr: {
+                       'aria-haspopup': 'dialog'
+               }
+               // Also the popover options, defined in Buttons.popover
+       },
+       split: {
+               text: function (dt) {
+                       return dt.i18n('buttons.split', 'Split');
+               },
+               className: 'buttons-split',
+               closeButton: false,
+               init: function (dt, button, config) {
+                       return button.attr('aria-expanded', false);
+               },
+               action: function (e, dt, button, config) {
+                       this.popover(config._collection, config);
+               },
+               attr: {
+                       'aria-haspopup': 'dialog'
+               }
+               // Also the popover options, defined in Buttons.popover
+       },
+       copy: function (dt, conf) {
+               if (_dtButtons.copyHtml5) {
+                       return 'copyHtml5';
+               }
+       },
+       csv: function (dt, conf) {
+               if (_dtButtons.csvHtml5 && _dtButtons.csvHtml5.available(dt, 
conf)) {
+                       return 'csvHtml5';
+               }
+       },
+       excel: function (dt, conf) {
+               if (_dtButtons.excelHtml5 && 
_dtButtons.excelHtml5.available(dt, conf)) {
+                       return 'excelHtml5';
+               }
+       },
+       pdf: function (dt, conf) {
+               if (_dtButtons.pdfHtml5 && _dtButtons.pdfHtml5.available(dt, 
conf)) {
+                       return 'pdfHtml5';
+               }
+       },
+       pageLength: function (dt) {
+               var lengthMenu = dt.settings()[0].aLengthMenu;
+               var vals = [];
+               var lang = [];
+               var text = function (dt) {
+                       return dt.i18n(
+                               'buttons.pageLength',
+                               {
+                                       '-1': 'Show all rows',
+                                       _: 'Show %d rows'
+                               },
+                               dt.page.len()
+                       );
+               };
+
+               // Support for DataTables 1.x 2D array
+               if (Array.isArray(lengthMenu[0])) {
+                       vals = lengthMenu[0];
+                       lang = lengthMenu[1];
+               }
+               else {
+                       for (var i = 0; i < lengthMenu.length; i++) {
+                               var option = lengthMenu[i];
+
+                               // Support for DataTables 2 object in the array
+                               if ($.isPlainObject(option)) {
+                                       vals.push(option.value);
+                                       lang.push(option.label);
+                               }
+                               else {
+                                       vals.push(option);
+                                       lang.push(option);
+                               }
+                       }
+               }
+
+               return {
+                       extend: 'collection',
+                       text: text,
+                       className: 'buttons-page-length',
+                       autoClose: true,
+                       buttons: $.map(vals, function (val, i) {
+                               return {
+                                       text: lang[i],
+                                       className: 'button-page-length',
+                                       action: function (e, dt) {
+                                               dt.page.len(val).draw();
+                                       },
+                                       init: function (dt, node, conf) {
+                                               var that = this;
+                                               var fn = function () {
+                                                       
that.active(dt.page.len() === val);
+                                               };
+
+                                               dt.on('length.dt' + 
conf.namespace, fn);
+                                               fn();
+                                       },
+                                       destroy: function (dt, node, conf) {
+                                               dt.off('length.dt' + 
conf.namespace);
+                                       }
+                               };
+                       }),
+                       init: function (dt, node, conf) {
+                               var that = this;
+                               dt.on('length.dt' + conf.namespace, function () 
{
+                                       that.text(conf.text);
+                               });
+                       },
+                       destroy: function (dt, node, conf) {
+                               dt.off('length.dt' + conf.namespace);
+                       }
+               };
+       },
+       spacer: {
+               style: 'empty',
+               spacer: true,
+               text: function (dt) {
+                       return dt.i18n('buttons.spacer', '');
+               }
+       }
+});
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 
*
+ * DataTables API
+ *
+ * For complete documentation, please refer to the docs/api directory or the
+ * DataTables site
+ */
+
+// Buttons group and individual button selector
+DataTable.Api.register('buttons()', function (group, selector) {
+       // Argument shifting
+       if (selector === undefined) {
+               selector = group;
+               group = undefined;
+       }
+
+       this.selector.buttonGroup = group;
+
+       var res = this.iterator(
+               true,
+               'table',
+               function (ctx) {
+                       if (ctx._buttons) {
+                               return Buttons.buttonSelector(
+                                       Buttons.instanceSelector(group, 
ctx._buttons),
+                                       selector
+                               );
+                       }
+               },
+               true
+       );
+
+       res._groupSelector = group;
+       return res;
+});
+
+// Individual button selector
+DataTable.Api.register('button()', function (group, selector) {
+       // just run buttons() and truncate
+       var buttons = this.buttons(group, selector);
+
+       if (buttons.length > 1) {
+               buttons.splice(1, buttons.length);
+       }
+
+       return buttons;
+});
+
+// Active buttons
+DataTable.Api.registerPlural('buttons().active()', 'button().active()', 
function (flag) {
+       if (flag === undefined) {
+               return this.map(function (set) {
+                       return set.inst.active(set.node);
+               });
+       }
+
+       return this.each(function (set) {
+               set.inst.active(set.node, flag);
+       });
+});
+
+// Get / set button action
+DataTable.Api.registerPlural('buttons().action()', 'button().action()', 
function (action) {
+       if (action === undefined) {
+               return this.map(function (set) {
+                       return set.inst.action(set.node);
+               });
+       }
+
+       return this.each(function (set) {
+               set.inst.action(set.node, action);
+       });
+});
+
+// Collection control
+DataTable.Api.registerPlural(
+       'buttons().collectionRebuild()',
+       'button().collectionRebuild()',
+       function (buttons) {
+               return this.each(function (set) {
+                       for (var i = 0; i < buttons.length; i++) {
+                               if (typeof buttons[i] === 'object') {
+                                       buttons[i].parentConf = set;
+                               }
+                       }
+                       set.inst.collectionRebuild(set.node, buttons);
+               });
+       }
+);
+
+// Enable / disable buttons
+DataTable.Api.register(['buttons().enable()', 'button().enable()'], function 
(flag) {
+       return this.each(function (set) {
+               set.inst.enable(set.node, flag);
+       });
+});
+
+// Disable buttons
+DataTable.Api.register(['buttons().disable()', 'button().disable()'], function 
() {
+       return this.each(function (set) {
+               set.inst.disable(set.node);
+       });
+});
+
+// Button index
+DataTable.Api.register('button().index()', function () {
+       var idx = null;
+
+       this.each(function (set) {
+               var res = set.inst.index(set.node);
+
+               if (res !== null) {
+                       idx = res;
+               }
+       });
+
+       return idx;
+});
+
+// Get button nodes
+DataTable.Api.registerPlural('buttons().nodes()', 'button().node()', function 
() {
+       var jq = $();
+
+       // jQuery will automatically reduce duplicates to a single entry
+       $(
+               this.each(function (set) {
+                       jq = jq.add(set.inst.node(set.node));
+               })
+       );
+
+       return jq;
+});
+
+// Get / set button processing state
+DataTable.Api.registerPlural('buttons().processing()', 
'button().processing()', function (flag) {
+       if (flag === undefined) {
+               return this.map(function (set) {
+                       return set.inst.processing(set.node);
+               });
+       }
+
+       return this.each(function (set) {
+               set.inst.processing(set.node, flag);
+       });
+});
+
+// Get / set button text (i.e. the button labels)
+DataTable.Api.registerPlural('buttons().text()', 'button().text()', function 
(label) {
+       if (label === undefined) {
+               return this.map(function (set) {
+                       return set.inst.text(set.node);
+               });
+       }
+
+       return this.each(function (set) {
+               set.inst.text(set.node, label);
+       });
+});
+
+// Trigger a button's action
+DataTable.Api.registerPlural('buttons().trigger()', 'button().trigger()', 
function () {
+       return this.each(function (set) {
+               set.inst.node(set.node).trigger('click');
+       });
+});
+
+// Button resolver to the popover
+DataTable.Api.register('button().popover()', function (content, options) {
+       return this.map(function (set) {
+               return set.inst._popover(content, this.button(this[0].node), 
options);
+       });
+});
+
+// Get the container elements
+DataTable.Api.register('buttons().containers()', function () {
+       var jq = $();
+       var groupSelector = this._groupSelector;
+
+       // We need to use the group selector directly, since if there are no 
buttons
+       // the result set will be empty
+       this.iterator(true, 'table', function (ctx) {
+               if (ctx._buttons) {
+                       var insts = Buttons.instanceSelector(groupSelector, 
ctx._buttons);
+
+                       for (var i = 0, ien = insts.length; i < ien; i++) {
+                               jq = jq.add(insts[i].container());
+                       }
+               }
+       });
+
+       return jq;
+});
+
+DataTable.Api.register('buttons().container()', function () {
+       // API level of nesting is `buttons()` so we can zip into the 
containers method
+       return this.containers().eq(0);
+});
+
+// Add a new button
+DataTable.Api.register('button().add()', function (idx, conf, draw) {
+       var ctx = this.context;
+
+       // Don't use `this` as it could be empty - select the instances directly
+       if (ctx.length) {
+               var inst = Buttons.instanceSelector(this._groupSelector, 
ctx[0]._buttons);
+
+               if (inst.length) {
+                       inst[0].add(conf, idx, draw);
+               }
+       }
+
+       return this.button(this._groupSelector, idx);
+});
+
+// Destroy the button sets selected
+DataTable.Api.register('buttons().destroy()', function () {
+       this.pluck('inst')
+               .unique()
+               .each(function (inst) {
+                       inst.destroy();
+               });
+
+       return this;
+});
+
+// Remove a button
+DataTable.Api.registerPlural('buttons().remove()', 'buttons().remove()', 
function () {
+       this.each(function (set) {
+               set.inst.remove(set.node);
+       });
+
+       return this;
+});
+
+// Information box that can be used by buttons
+var _infoTimer;
+DataTable.Api.register('buttons.info()', function (title, message, time) {
+       var that = this;
+
+       if (title === false) {
+               this.off('destroy.btn-info');
+               _fadeOut($('#datatables_buttons_info'), 400, function () {
+                       $(this).remove();
+               });
+               clearTimeout(_infoTimer);
+               _infoTimer = null;
+
+               return this;
+       }
+
+       if (_infoTimer) {
+               clearTimeout(_infoTimer);
+       }
+
+       if ($('#datatables_buttons_info').length) {
+               $('#datatables_buttons_info').remove();
+       }
+
+       title = title ? '<h2>' + title + '</h2>' : '';
+
+       _fadeIn(
+               $('<div id="datatables_buttons_info" class="dt-button-info"/>')
+                       .html(title)
+                       .append($('<div/>')[typeof message === 'string' ? 
'html' : 'append'](message))
+                       .css('display', 'none')
+                       .appendTo('body')
+       );
+
+       if (time !== undefined && time !== 0) {
+               _infoTimer = setTimeout(function () {
+                       that.buttons.info(false);
+               }, time);
+       }
+
+       this.on('destroy.btn-info', function () {
+               that.buttons.info(false);
+       });
+
+       return this;
+});
+
+// Get data from the table for export - this is common to a number of plug-in
+// buttons so it is included in the Buttons core library
+DataTable.Api.register('buttons.exportData()', function (options) {
+       if (this.context.length) {
+               return _exportData(new DataTable.Api(this.context[0]), options);
+       }
+});
+
+// Get information about the export that is common to many of the export data
+// types (DRY)
+DataTable.Api.register('buttons.exportInfo()', function (conf) {
+       if (!conf) {
+               conf = {};
+       }
+
+       return {
+               filename: _filename(conf),
+               title: _title(conf),
+               messageTop: _message(this, conf.message || conf.messageTop, 
'top'),
+               messageBottom: _message(this, conf.messageBottom, 'bottom')
+       };
+});
+
+/**
+ * Get the file name for an exported file.
+ *
+ * @param {object}     config Button configuration
+ * @param {boolean} incExtension Include the file name extension
+ */
+var _filename = function (config) {
+       // Backwards compatibility
+       var filename =
+               config.filename === '*' &&
+               config.title !== '*' &&
+               config.title !== undefined &&
+               config.title !== null &&
+               config.title !== ''
+                       ? config.title
+                       : config.filename;
+
+       if (typeof filename === 'function') {
+               filename = filename();
+       }
+
+       if (filename === undefined || filename === null) {
+               return null;
+       }
+
+       if (filename.indexOf('*') !== -1) {
+               filename = filename.replace('*', $('head > 
title').text()).trim();
+       }
+
+       // Strip characters which the OS will object to
+       filename = filename.replace(/[^a-zA-Z0-9_\u00A1-\uFFFF\.,\-_ !\(\)]/g, 
'');
+
+       var extension = _stringOrFunction(config.extension);
+       if (!extension) {
+               extension = '';
+       }
+
+       return filename + extension;
+};
+
+/**
+ * Simply utility method to allow parameters to be given as a function
+ *
+ * @param {undefined|string|function} option Option
+ * @return {null|string} Resolved value
+ */
+var _stringOrFunction = function (option) {
+       if (option === null || option === undefined) {
+               return null;
+       }
+       else if (typeof option === 'function') {
+               return option();
+       }
+       return option;
+};
+
+/**
+ * Get the title for an exported file.
+ *
+ * @param {object} config      Button configuration
+ */
+var _title = function (config) {
+       var title = _stringOrFunction(config.title);
+
+       return title === null
+               ? null
+               : title.indexOf('*') !== -1
+               ? title.replace('*', $('head > title').text() || 'Exported 
data')
+               : title;
+};
+
+var _message = function (dt, option, position) {
+       var message = _stringOrFunction(option);
+       if (message === null) {
+               return null;
+       }
+
+       var caption = $('caption', dt.table().container()).eq(0);
+       if (message === '*') {
+               var side = caption.css('caption-side');
+               if (side !== position) {
+                       return null;
+               }
+
+               return caption.length ? caption.text() : '';
+       }
+
+       return message;
+};
+
+var _exportTextarea = $('<textarea/>')[0];
+var _exportData = function (dt, inOpts) {
+       var config = $.extend(
+               true,
+               {},
+               {
+                       rows: null,
+                       columns: '',
+                       modifier: {
+                               search: 'applied',
+                               order: 'applied'
+                       },
+                       orthogonal: 'display',
+                       stripHtml: true,
+                       stripNewlines: true,
+                       decodeEntities: true,
+                       trim: true,
+                       format: {
+                               header: function (d) {
+                                       return Buttons.stripData(d, config);
+                               },
+                               footer: function (d) {
+                                       return Buttons.stripData(d, config);
+                               },
+                               body: function (d) {
+                                       return Buttons.stripData(d, config);
+                               }
+                       },
+                       customizeData: null
+               },
+               inOpts
+       );
+
+       var header = dt
+               .columns(config.columns)
+               .indexes()
+               .map(function (idx) {
+                       var el = dt.column(idx).header();
+                       return config.format.header(el.innerHTML, idx, el);
+               })
+               .toArray();
+
+       var footer = dt.table().footer()
+               ? dt
+                               .columns(config.columns)
+                               .indexes()
+                               .map(function (idx) {
+                                       var el = dt.column(idx).footer();
+                                       return config.format.footer(el ? 
el.innerHTML : '', idx, el);
+                               })
+                               .toArray()
+               : null;
+
+       // If Select is available on this table, and any rows are selected, 
limit the export
+       // to the selected rows. If no rows are selected, all rows will be 
exported. Specify
+       // a `selected` modifier to control directly.
+       var modifier = $.extend({}, config.modifier);
+       if (dt.select && typeof dt.select.info === 'function' && 
modifier.selected === undefined) {
+               if (dt.rows(config.rows, $.extend({ selected: true }, 
modifier)).any()) {
+                       $.extend(modifier, { selected: true });
+               }
+       }
+
+       var rowIndexes = dt.rows(config.rows, modifier).indexes().toArray();
+       var selectedCells = dt.cells(rowIndexes, config.columns);
+       var cells = selectedCells.render(config.orthogonal).toArray();
+       var cellNodes = selectedCells.nodes().toArray();
+
+       var columns = header.length;
+       var rows = columns > 0 ? cells.length / columns : 0;
+       var body = [];
+       var cellCounter = 0;
+
+       for (var i = 0, ien = rows; i < ien; i++) {
+               var row = [columns];
+
+               for (var j = 0; j < columns; j++) {
+                       row[j] = config.format.body(cells[cellCounter], i, j, 
cellNodes[cellCounter]);
+                       cellCounter++;
+               }
+
+               body[i] = row;
+       }
+
+       var data = {
+               header: header,
+               footer: footer,
+               body: body
+       };
+
+       if (config.customizeData) {
+               config.customizeData(data);
+       }
+
+       return data;
+};
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 
*
+ * DataTables interface
+ */
+
+// Attach to DataTables objects for global access
+$.fn.dataTable.Buttons = Buttons;
+$.fn.DataTable.Buttons = Buttons;
+
+// DataTables creation - check if the buttons have been defined for this table,
+// they will have been if the `B` option was used in `dom`, otherwise we should
+// create the buttons instance here so they can be inserted into the document
+// using the API. Listen for `init` for compatibility with pre 1.10.10, but to
+// be removed in future.
+$(document).on('init.dt plugin-init.dt', function (e, settings) {
+       if (e.namespace !== 'dt') {
+               return;
+       }
+
+       var opts = settings.oInit.buttons || DataTable.defaults.buttons;
+
+       if (opts && !settings._buttons) {
+               new Buttons(settings, opts).container();
+       }
+});
+
+function _init(settings, options) {
+       var api = new DataTable.Api(settings);
+       var opts = options ? options : api.init().buttons || 
DataTable.defaults.buttons;
+
+       return new Buttons(api, opts).container();
+}
+
+// DataTables `dom` feature option
+DataTable.ext.feature.push({
+       fnInit: _init,
+       cFeature: 'B'
+});
+
+// DataTables 2 layout feature
+if (DataTable.ext.features) {
+       DataTable.ext.features.register('buttons', _init);
+}
+
+
+return DataTable;
+}));
diff --git 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/server_process_common.js
 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/server_process_common.js
index c711f6e8db..bdb9361145 100644
--- 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/server_process_common.js
+++ 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/server_process_common.js
@@ -252,6 +252,14 @@ function createDataTable(table, storageKey) {
         data: getStoredRows(storageKey)
       });
     },
+    "buttons": [{
+      "extend": 'colvis',
+      "text": '<i class="bi bi-gear"></i>',
+      "titleAttr": 'Columns'
+    }],
+    "dom": '<"row"<"col-sm-12 col-md-4"l><"col-sm-12 col-md-6"f><"col-sm-12 
col-md-2"B>>' +
+      '<"row dt-row"<"col-sm-12"rt>>' +
+      '<"row"<"col-sm-12 col-md-5"i><"col-sm-12 col-md-7"p>>',
     "stateSave": true,
     "colReorder": true,
     "columnDefs": [{
diff --git 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/compactors.ftl
 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/compactors.ftl
index 71944f81e7..bed7793e2e 100644
--- 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/compactors.ftl
+++ 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/compactors.ftl
@@ -23,10 +23,12 @@
       </div>
       <div class="row">
         <div class="col-xs-12">
+          <span class="table-caption">Compactors</span>
+          <br />
+          <span class="table-subcaption">The following Compactors reported 
status.</span>
+          <br />
+          <br />
           <table id="compactorsTable" class="table caption-top table-bordered 
table-striped table-condensed">
-            <caption><span class="table-caption">Compactors</span><br />
-              <span class="table-subcaption">The following Compactors reported 
status.</span><br />
-            </caption>
             <#include "table_loading.ftl" >
           </table>
         </div>
diff --git 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/coordinator.ftl
 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/coordinator.ftl
index 11d700f30f..70564e5d11 100644
--- 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/coordinator.ftl
+++ 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/coordinator.ftl
@@ -18,19 +18,22 @@
     under the License.
 
 -->
+    <br />
+    <span class="table-caption">Compaction Coordinator Activity</span>
     <br />
     <table id="coordinators" class="table caption-top table-bordered 
table-striped table-condensed">
-        <caption><span class="table-caption">Compaction Coordinator 
Activity</span><br /></caption>
-        <#include "table_loading.ftl" >
+      <#include "table_loading.ftl" >
     </table>
     <br />
+    <span class="table-caption">Compaction Coordinator Queues</span>
+    <br />
     <table id="coordinator_queues" class="table caption-top table-bordered 
table-striped table-condensed">
-        <caption><span class="table-caption">Compaction Coordinator 
Queues</span><br /></caption>
-        <#include "table_loading.ftl" >
+      <#include "table_loading.ftl" >
     </table>
     <br />
+    <span class="table-caption">Running Compactions By Table</span>
+    <br />
     <table id="table_running" class="table caption-top table-bordered 
table-striped table-condensed">
-        <caption><span class="table-caption">Running Compactions By 
Table</span><br /></caption>
         <thead>
           <tr>
             <th>Table Name</th>
@@ -40,8 +43,9 @@
         <tbody></tbody>
     </table>
     <br />
+    <span class="table-caption">Running Compactions By Queue</span>
+    <br />
     <table id="queue_running" class="table caption-top table-bordered 
table-striped table-condensed">
-        <caption><span class="table-caption">Running Compactions By 
Queue</span><br /></caption>
         <thead>
           <tr>
             <th>Queue Name</th>
diff --git 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/default.ftl
 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/default.ftl
index 1e2ad1e5ef..163ebe8202 100644
--- 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/default.ftl
+++ 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/default.ftl
@@ -42,12 +42,16 @@
       <script src="resources/external/jquery/jquery-3.7.1.js"></script>
       <script 
src="resources/external/bootstrap/js/bootstrap.bundle.js"></script>
       <script 
src="resources/external/datatables/js/jquery.dataTables.js"></script>
+      <script 
src="resources/external/datatables/js/dataTables.buttons.js"></script>
       <script 
src="resources/external/datatables/js/dataTables.colReorder.js"></script>
       <script 
src="resources/external/datatables/js/dataTables.bootstrap5.js"></script>
+      <script 
src="resources/external/datatables/js/buttons.bootstrap5.js"></script>
       <script 
src="resources/external/datatables/js/colReorder.bootstrap5.js"></script>
+      <script 
src="resources/external/datatables/js/buttons.colVis.js"></script>
       <link rel="stylesheet" 
href="resources/external/bootstrap/css/bootstrap.css" />
       <link rel="stylesheet" 
href="resources/external/bootstrap/css/bootstrap-icons.css" />
       <link rel="stylesheet" 
href="resources/external/datatables/css/dataTables.bootstrap5.css" />
+      <link rel="stylesheet" 
href="resources/external/datatables/css/buttons.bootstrap5.css" />
       <link rel="stylesheet" 
href="resources/external/datatables/css/colReorder.bootstrap5.css" />
     </#if>
 
diff --git 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/gc.ftl
 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/gc.ftl
index 19e6d317bb..93bb069fa6 100644
--- 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/gc.ftl
+++ 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/gc.ftl
@@ -22,20 +22,23 @@
         <div id="gc-banner-message" class="alert" role="alert"></div>
       </div>
       <div>
+        <span class="table-caption">Garbage Collector</span>
+        <br />
         <table id="gc-server" class="table caption-top table-bordered 
table-striped table-condensed">
-          <caption><span class="table-caption">Garbage Collector</span><br 
/></caption>
           <#include "table_loading.ftl" >
         </table>
       </div>
       <div>
+        <span class="table-caption">File Collection</span>
+        <br />
         <table id="gc-file" class="table caption-top table-bordered 
table-striped table-condensed">
-          <caption><span class="table-caption">File Collection</span><br 
/></caption>
           <#include "table_loading.ftl" >
         </table>
       </div>
-            <div>
+      <div>
+        <span class="table-caption">Wal Collection</span>
+        <br />
         <table id="gc-wal" class="table caption-top table-bordered 
table-striped table-condensed">
-          <caption><span class="table-caption">Wal Collection</span><br 
/></caption>
           <#include "table_loading.ftl" >
         </table>
       </div>
diff --git 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/manager.ftl
 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/manager.ftl
index 503ade72d5..f011fd46fd 100644
--- 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/manager.ftl
+++ 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/manager.ftl
@@ -27,13 +27,15 @@
     <div id="managerStateBanner" style="display: none;">
         <div id="manager-state-message" class="alert alert-warning" 
role="alert"></div>
     </div>
+    <span class="table-caption">Managers</span>
+    <br />
     <table id="managers" class="table caption-top table-bordered table-striped 
table-condensed">
-        <caption><span class="table-caption">Managers</span><br /></caption>
         <#include "table_loading.ftl" >
     </table>
     <br />
+    <span class="table-caption">Manager Fate Activity</span>
+    <br />
     <table id="managers_fate" class="table caption-top table-bordered 
table-striped table-condensed">
-        <caption><span class="table-caption">Manager Fate Activity</span><br 
/></caption>
         <#include "table_loading.ftl" >
     </table>
     <br />
diff --git 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/sservers.ftl
 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/sservers.ftl
index 3ac0891e50..b506687468 100644
--- 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/sservers.ftl
+++ 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/sservers.ftl
@@ -23,10 +23,12 @@
     </div>
     <div class="row">
       <div class="col-xs-12">
+        <span class="table-caption">Scan Servers</span>
+        <br />
+        <span class="table-subcaption">The following Scan Servers reported 
status.</span>
+        <br />
+        <br />
         <table id="sservers" class="table caption-top table-bordered 
table-striped table-condensed">
-          <caption><span class="table-caption">Scan Servers</span><br />
-            <span class="table-subcaption">The following Scan Servers reported 
status.</span><br />
-          </caption>
           <#include "table_loading.ftl" >
         </table>
       </div>
diff --git 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/tservers.ftl
 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/tservers.ftl
index 2973391603..1978759908 100644
--- 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/tservers.ftl
+++ 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/tservers.ftl
@@ -26,10 +26,12 @@
     </div>    
     <div class="row">
       <div class="col-xs-12">
+        <span class="table-caption">Tablet Servers</span>
+        <br />
+        <span class="table-subcaption">The following Tablet Servers reported 
status.</span>
+        <br />
+        <br />
         <table id="tservers" class="table caption-top table-bordered 
table-striped table-condensed">
-          <caption><span class="table-caption">Tablet Servers</span><br />
-            <span class="table-subcaption">The following Tablet Servers 
reported status.</span><br />
-          </caption>
           <#include "table_loading.ftl" >
         </table>
       </div>

Reply via email to