Dashboards & Visualizations

How to make a second download button

hallt3
Path Finder

Does anybody know of a way to make a second download button (could use the same modal as the official dashboard one)?
I've tried playing around with some custom JS and tried messing with the data_cid field a bit.
Not sure how to proceed.

0 Karma
1 Solution

pramit46
Contributor

Hi @hallt3

I have a dashboard that serves a purpose almost similar to yours.

Let me try to explain it here.. See if that helps in your case.

For this, I have a used a JS file called download_test.js:

//$("#modal3").append($("<button class=\"btn btn-primary\">Launch Modal</button>").click(function() {
$("#download_file_div").append($("<button id=\"download_file_button\">Download File</button>").click(function() {
    // The require will let us tell the browser to load /static/app/MyTest/Modal.js with the name "Modal"
    require(['jquery',
        '/static/app/MyTest/Modal.js',
        'underscore',
        'splunkjs/mvc',
        'splunkjs/mvc/utils',
        'splunkjs/mvc/tokenutils',
        'splunkjs/mvc/searchmanager',
        'splunkjs/mvc/simplexml/ready!'
    ], function($,Modal,_, mvc, utils, TokenUtils, SearchManager) {
        // Now we initialize the Modal itself
        var myModal = new Modal("modal1", {
            title: "Alert!!!",
            backdrop: 'static',
            keyboard: false,
            destroyOnHide: true,
            type: 'normal'
        });


        // Create a Search Manager
        var exportSearch = new SearchManager({
             earliest_time: "-24h",
             latest_time: "now",
             search: "index=_internal |top 5  sourcetype", //some dummy search to fill up the content of the csv
             autostart: false
    });     

        $(myModal.$el).on("hide", function() {})
        myModal.body.append($('<p>Do you want to download the session details?</p>'));
        myModal.footer.append($('<button>').attr({
            type: 'button',
            'data-dismiss': 'modal'
        }).addClass('btn btn-primary icon-export').on('click', function(_, $, mvc, utils, TokenUtils) {      
        exportSearch.startSearch();
        var exportResults = exportSearch.data("results");
        exportResults.on("data", function () {
            //var data = exportResults.collection().toJSON();
            var data = exportResults.data().fields +"\n";
            console.log("d1: "+data);
            console.log("exportResults.data().rows: "+exportResults.data().rows);
            jQuery.each(exportResults.data().rows, function(row){
                data=data+"\n"+exportResults.data().rows[row];
            });
            console.log("d2: "+data);
            // download data as file
            var hiddenElement = document.createElement('a');
            //hiddenElement.href = 'data:attachment/csv,' + encodeURI(JSON.stringify(data, null, 2));
            hiddenElement.href = 'data:attachment/csv,' + encodeURI(data);
            hiddenElement.target = '_blank';
            hiddenElement.download = 'exportData.csv';
            hiddenElement.click();  
        });
        }), $('<button>').attr({
            type: 'button',
            'data-dismiss': 'modal'
        }).addClass('btn btn-primary').text('Never Mind').on('click', function() {
            //Some logic for the 'Never Mind' button, if required.. May be the logout option.
            var logoutURL = window.location.protocol
            +"//"+window.location.hostname
            +":8000"
            +"/en-US/account/logout"
        //window.location=logoutURL;//Redirect the user to logout page if he clicks on 'Never Mind', Uncomment if you want. 
        }))
        myModal.show(); // Launch it!
    })
}))

Please note that this uses the same Modal.js that is shipped with splunk (I got it from some other apps, though). I'll still provide the content of that Modal.js, here:

'use strict';

/*console.log("Here's my code path", document.currentScript)*/
//console.log("Trying again", [].slice.call(document.querySelectorAll('script[src]')).pop().src.replace(/.*?static\/app\/([^\/]*).*/, "$1"))
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function(obj) { return typeof obj; } : function(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; };

var _createClass = function() {
    function defineProperties(target, props) {
        for (var i = 0; i < props.length; i++) {
            var descriptor = props[i];
            descriptor.enumerable = descriptor.enumerable || false;
            descriptor.configurable = true;
            if ("value" in descriptor) descriptor.writable = true;
            Object.defineProperty(target, descriptor.key, descriptor);
        }
    }
    return function(Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; };
}();

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _setModalMaxHeight(element) {
    this.$element = $(element);
    this.$content = this.$element.find('.modal-content');
    var borderWidth = this.$content.outerHeight() - this.$content.innerHeight();
    var dialogMargin = $(window).width() < 768 ? 20 : 60;
    var contentHeight = $(window).height() - (dialogMargin + borderWidth);
    var headerHeight = this.$element.find('.modal-header').outerHeight() || 0;
    var footerHeight = this.$element.find('.modal-footer').outerHeight() || 0;
    var maxHeight = contentHeight - (headerHeight + footerHeight);

    this.$content.css({
        'overflow': 'hidden'
    });

    this.$element
        .find('.modal-body').css({
            'max-height': maxHeight,
            'overflow-y': 'auto'
        });
}

define(['underscore'], function(_) {
    return function() {
        /**
         * A utility wrapper around Bootstrap's modal.
         * @param {string|object} id                            Either an id or a jQuery element that contains the id in its "data-target" attribute
         * @param {object}         [options]                    Bootstrap modal options
         * @param {boolean|string} [options.backdrop]           Whether or not to show a backdrop, or the string "static" to show a backdrop that doesn't close the modal when clicked
         * @param {boolean}        [options.keyboard]           Whether or not the escape key clsoes the modal
         * @param {boolean}        [options.show=false]         Whether or not to show the modal when it's created
         * @param {string}         [options.type='normal']      Can be 'normal', 'wide', or 'noPadding'
         * @param {string}         [options.title]              The modal's title
         * @param {boolean}        [options.destroyOnHide=true] Destroy the modal when it's hidden
         * @returns {element}
         */
        function Modal(id, options) {
            var _this = this;

            _classCallCheck(this, Modal);

            var modalOptions = _.extend({ show: false }, options);

            // if "id" is the element that triggers the modal display, extract the actual id from it; otherwise use it as-is
            var modalId = id != null && (typeof id === 'undefined' ? 'undefined' : _typeof(id)) === 'object' && id.jquery != null ? id.attr('data-target').slice(1) : id;

            var header = $('<div>').addClass('modal-header');

            var headerCloseButton = $('<button>').addClass('close').attr({
                'type': 'button',
                'data-dismiss': 'modal',
                'aria-label': 'Close'
            }).append($('<span>').attr('aria-hidden', true).text('&times;'));

            this.title = $('<h3>').addClass('modal-title');

            this.body = $('<div>').addClass('modal-body');

            this.footer = $('<div>').addClass('modal-footer');

            // Multiselect can grow large and step over footer causing issues clicking button in footer
            this.footer.css('position', 'relative');
            this.footer.css('z-index', 1);

            /*console.log("Here's my code path 2", document.currentScript)*/


            this.$el = $('<div>').addClass('modal hide fade mlts-modal').attr('id', modalId).append($('<div>').addClass('modal-dialog').append($('<div>').addClass('modal-content').append(header.append(headerCloseButton, this.title), this.body, this.footer)));

            if (modalOptions.title != null) this.setTitle(modalOptions.title);

            if (modalOptions.type === 'wide') this.$el.addClass('modal-wide');
            else if (modalOptions.type === 'noPadding') this.$el.addClass('mlts-modal-no-padding');

            // remove the modal from the dom after it's hidden
            if (modalOptions.destroyOnHide !== false) {
                this.$el.on('hidden.bs.modal', function() {
                    return _this.$el.remove();
                });
            }

            this.$el.on('show.bs.modal', function() {
                $(this).show();
                _setModalMaxHeight(this);
            });

            $(window).resize(function() {
                if ($('.modal.in').length != 0) {
                    _setModalMaxHeight($('.modal.in'));
                }
            });

            this.$el.modal(modalOptions);
        }

        _createClass(Modal, [{
            key: 'setTitle',
            value: function setTitle(titleText) {
                this.title.text(titleText);
            }
        }, {
            key: 'setAlert',
            value: function setAlert(alertMessage, alertType) {
                if (this.alert == null) {
                    this.alert = $('<div>').addClass('mlts-modal-alert');
                    this.body.prepend(this.alert);
                }

                //Messages.setAlert(this.alert, alertMessage, alertType, undefined, true);
            }
        }, {
            key: 'removeAlert',
            value: function removeAlert() {
                //Messages.removeAlert(this.alert, true);
            }
        }, {
            key: 'show',
            value: function show() {
                this.$el.modal('show');
            }
        }, {
            key: 'hide',
            value: function hide() {
                this.$el.modal('hide');
            }
        }]);

        return Modal;
    }();
});


//# sourceURL=Modal.js

Finally, the HTML Code:

<dashboard script="download_test.js">
  <label>Download Test</label>
 <row id="submit_button">
    <panel id="btnPanel">
      <html>
        <div class="divTable">
            <div class="divTableBody">
              <div class="divTableRow">
                <div class="divTableCell" style="width: 35%;" align="left">
                  <div id="download_file_div" style="display: table-cell;padding-left: 5px;">
                  </div>
                </div>
                <div class="divTableCell" style="width: 65%" align="left"/>
              </div>
              </div>
          </div>
      </html>
    </panel>
  </row>
</dashboard>

This dashboard should populate the Download File Button dynamically, as soon as you open the dashboard. Then while you click on that button, it should bring some modal like below:

alt text

Just click on that download icon, and the JS will trigger the search query index=_internal |top 5 sourcetype (you can always change it according to your convenience) and bring up the SaveAs dialog box. The Default filename is exportData.csv. Click on Never Mind button and the modal will disappear. Alternatively, you can also click on the X button on the modal.

Do let me know if you have any queries. That download_test.js file was used for some internal requirements, hence it is not properly formatted. If you face any difficulties understanding it, I can try to explain it.

View solution in original post

pramit46
Contributor

Hi @hallt3

I have a dashboard that serves a purpose almost similar to yours.

Let me try to explain it here.. See if that helps in your case.

For this, I have a used a JS file called download_test.js:

//$("#modal3").append($("<button class=\"btn btn-primary\">Launch Modal</button>").click(function() {
$("#download_file_div").append($("<button id=\"download_file_button\">Download File</button>").click(function() {
    // The require will let us tell the browser to load /static/app/MyTest/Modal.js with the name "Modal"
    require(['jquery',
        '/static/app/MyTest/Modal.js',
        'underscore',
        'splunkjs/mvc',
        'splunkjs/mvc/utils',
        'splunkjs/mvc/tokenutils',
        'splunkjs/mvc/searchmanager',
        'splunkjs/mvc/simplexml/ready!'
    ], function($,Modal,_, mvc, utils, TokenUtils, SearchManager) {
        // Now we initialize the Modal itself
        var myModal = new Modal("modal1", {
            title: "Alert!!!",
            backdrop: 'static',
            keyboard: false,
            destroyOnHide: true,
            type: 'normal'
        });


        // Create a Search Manager
        var exportSearch = new SearchManager({
             earliest_time: "-24h",
             latest_time: "now",
             search: "index=_internal |top 5  sourcetype", //some dummy search to fill up the content of the csv
             autostart: false
    });     

        $(myModal.$el).on("hide", function() {})
        myModal.body.append($('<p>Do you want to download the session details?</p>'));
        myModal.footer.append($('<button>').attr({
            type: 'button',
            'data-dismiss': 'modal'
        }).addClass('btn btn-primary icon-export').on('click', function(_, $, mvc, utils, TokenUtils) {      
        exportSearch.startSearch();
        var exportResults = exportSearch.data("results");
        exportResults.on("data", function () {
            //var data = exportResults.collection().toJSON();
            var data = exportResults.data().fields +"\n";
            console.log("d1: "+data);
            console.log("exportResults.data().rows: "+exportResults.data().rows);
            jQuery.each(exportResults.data().rows, function(row){
                data=data+"\n"+exportResults.data().rows[row];
            });
            console.log("d2: "+data);
            // download data as file
            var hiddenElement = document.createElement('a');
            //hiddenElement.href = 'data:attachment/csv,' + encodeURI(JSON.stringify(data, null, 2));
            hiddenElement.href = 'data:attachment/csv,' + encodeURI(data);
            hiddenElement.target = '_blank';
            hiddenElement.download = 'exportData.csv';
            hiddenElement.click();  
        });
        }), $('<button>').attr({
            type: 'button',
            'data-dismiss': 'modal'
        }).addClass('btn btn-primary').text('Never Mind').on('click', function() {
            //Some logic for the 'Never Mind' button, if required.. May be the logout option.
            var logoutURL = window.location.protocol
            +"//"+window.location.hostname
            +":8000"
            +"/en-US/account/logout"
        //window.location=logoutURL;//Redirect the user to logout page if he clicks on 'Never Mind', Uncomment if you want. 
        }))
        myModal.show(); // Launch it!
    })
}))

Please note that this uses the same Modal.js that is shipped with splunk (I got it from some other apps, though). I'll still provide the content of that Modal.js, here:

'use strict';

/*console.log("Here's my code path", document.currentScript)*/
//console.log("Trying again", [].slice.call(document.querySelectorAll('script[src]')).pop().src.replace(/.*?static\/app\/([^\/]*).*/, "$1"))
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function(obj) { return typeof obj; } : function(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; };

var _createClass = function() {
    function defineProperties(target, props) {
        for (var i = 0; i < props.length; i++) {
            var descriptor = props[i];
            descriptor.enumerable = descriptor.enumerable || false;
            descriptor.configurable = true;
            if ("value" in descriptor) descriptor.writable = true;
            Object.defineProperty(target, descriptor.key, descriptor);
        }
    }
    return function(Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; };
}();

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _setModalMaxHeight(element) {
    this.$element = $(element);
    this.$content = this.$element.find('.modal-content');
    var borderWidth = this.$content.outerHeight() - this.$content.innerHeight();
    var dialogMargin = $(window).width() < 768 ? 20 : 60;
    var contentHeight = $(window).height() - (dialogMargin + borderWidth);
    var headerHeight = this.$element.find('.modal-header').outerHeight() || 0;
    var footerHeight = this.$element.find('.modal-footer').outerHeight() || 0;
    var maxHeight = contentHeight - (headerHeight + footerHeight);

    this.$content.css({
        'overflow': 'hidden'
    });

    this.$element
        .find('.modal-body').css({
            'max-height': maxHeight,
            'overflow-y': 'auto'
        });
}

define(['underscore'], function(_) {
    return function() {
        /**
         * A utility wrapper around Bootstrap's modal.
         * @param {string|object} id                            Either an id or a jQuery element that contains the id in its "data-target" attribute
         * @param {object}         [options]                    Bootstrap modal options
         * @param {boolean|string} [options.backdrop]           Whether or not to show a backdrop, or the string "static" to show a backdrop that doesn't close the modal when clicked
         * @param {boolean}        [options.keyboard]           Whether or not the escape key clsoes the modal
         * @param {boolean}        [options.show=false]         Whether or not to show the modal when it's created
         * @param {string}         [options.type='normal']      Can be 'normal', 'wide', or 'noPadding'
         * @param {string}         [options.title]              The modal's title
         * @param {boolean}        [options.destroyOnHide=true] Destroy the modal when it's hidden
         * @returns {element}
         */
        function Modal(id, options) {
            var _this = this;

            _classCallCheck(this, Modal);

            var modalOptions = _.extend({ show: false }, options);

            // if "id" is the element that triggers the modal display, extract the actual id from it; otherwise use it as-is
            var modalId = id != null && (typeof id === 'undefined' ? 'undefined' : _typeof(id)) === 'object' && id.jquery != null ? id.attr('data-target').slice(1) : id;

            var header = $('<div>').addClass('modal-header');

            var headerCloseButton = $('<button>').addClass('close').attr({
                'type': 'button',
                'data-dismiss': 'modal',
                'aria-label': 'Close'
            }).append($('<span>').attr('aria-hidden', true).text('&times;'));

            this.title = $('<h3>').addClass('modal-title');

            this.body = $('<div>').addClass('modal-body');

            this.footer = $('<div>').addClass('modal-footer');

            // Multiselect can grow large and step over footer causing issues clicking button in footer
            this.footer.css('position', 'relative');
            this.footer.css('z-index', 1);

            /*console.log("Here's my code path 2", document.currentScript)*/


            this.$el = $('<div>').addClass('modal hide fade mlts-modal').attr('id', modalId).append($('<div>').addClass('modal-dialog').append($('<div>').addClass('modal-content').append(header.append(headerCloseButton, this.title), this.body, this.footer)));

            if (modalOptions.title != null) this.setTitle(modalOptions.title);

            if (modalOptions.type === 'wide') this.$el.addClass('modal-wide');
            else if (modalOptions.type === 'noPadding') this.$el.addClass('mlts-modal-no-padding');

            // remove the modal from the dom after it's hidden
            if (modalOptions.destroyOnHide !== false) {
                this.$el.on('hidden.bs.modal', function() {
                    return _this.$el.remove();
                });
            }

            this.$el.on('show.bs.modal', function() {
                $(this).show();
                _setModalMaxHeight(this);
            });

            $(window).resize(function() {
                if ($('.modal.in').length != 0) {
                    _setModalMaxHeight($('.modal.in'));
                }
            });

            this.$el.modal(modalOptions);
        }

        _createClass(Modal, [{
            key: 'setTitle',
            value: function setTitle(titleText) {
                this.title.text(titleText);
            }
        }, {
            key: 'setAlert',
            value: function setAlert(alertMessage, alertType) {
                if (this.alert == null) {
                    this.alert = $('<div>').addClass('mlts-modal-alert');
                    this.body.prepend(this.alert);
                }

                //Messages.setAlert(this.alert, alertMessage, alertType, undefined, true);
            }
        }, {
            key: 'removeAlert',
            value: function removeAlert() {
                //Messages.removeAlert(this.alert, true);
            }
        }, {
            key: 'show',
            value: function show() {
                this.$el.modal('show');
            }
        }, {
            key: 'hide',
            value: function hide() {
                this.$el.modal('hide');
            }
        }]);

        return Modal;
    }();
});


//# sourceURL=Modal.js

Finally, the HTML Code:

<dashboard script="download_test.js">
  <label>Download Test</label>
 <row id="submit_button">
    <panel id="btnPanel">
      <html>
        <div class="divTable">
            <div class="divTableBody">
              <div class="divTableRow">
                <div class="divTableCell" style="width: 35%;" align="left">
                  <div id="download_file_div" style="display: table-cell;padding-left: 5px;">
                  </div>
                </div>
                <div class="divTableCell" style="width: 65%" align="left"/>
              </div>
              </div>
          </div>
      </html>
    </panel>
  </row>
</dashboard>

This dashboard should populate the Download File Button dynamically, as soon as you open the dashboard. Then while you click on that button, it should bring some modal like below:

alt text

Just click on that download icon, and the JS will trigger the search query index=_internal |top 5 sourcetype (you can always change it according to your convenience) and bring up the SaveAs dialog box. The Default filename is exportData.csv. Click on Never Mind button and the modal will disappear. Alternatively, you can also click on the X button on the modal.

Do let me know if you have any queries. That download_test.js file was used for some internal requirements, hence it is not properly formatted. If you face any difficulties understanding it, I can try to explain it.

hallt3
Path Finder

Is there any way to escape the results? Some of my data has commas, newlines and it is breaking the results.

0 Karma

pramit46
Contributor

do you mean your search output has newlines and commas? Can you post a sample?

0 Karma

hallt3
Path Finder

The field in the search has a comma.

Street
'1, John St.' becomes 2 fields in the csv because of the ',' . This throws the whole row off.

The newlines are less of an issue as I can reliable remove them with a regex but the commas I can't. The rest of the commas are important to the structure of the row. I was wondering if it was possible to get individual field values and not just the whole row.

0 Karma

pramit46
Contributor

It does not seem to be a splunk problem.
Did you try using an extra pair of "? I mean try to populate your result as:
""1, john St."" instead of "1, John St."
See if that helps....

0 Karma

hallt3
Path Finder

You can escape it, sure but it's a horrible pain in the but to do that for 100+ fields in splunk"

0 Karma

hallt3
Path Finder

This is great! And I found a way to link up the hard coded search to the search on the dashboard (sid).

  1. Name the search in question ()
  2. Add 'splunkjs/mvc/tableview' to require
  3. Replace the export search var (~24) with the following:

    // Access the "default" token model
    var tokens = splunkjs.mvc.Components.get("default");
    
    // Retrieve the value of a token $mytoken$
    var download_sid = tokens.get("download_sid");
    
    // log
    console.log(download_sid);
    
    var exportSearch = splunkjs.mvc.Components.get(download_sid);
    console.log(exportSearch)
    
  4. Remove exportSearch.startSearch(); from downloadtest.js (~37)

pramit46
Contributor

If you had removed the startSearch, then how are you triggering the search?

0 Karma

hallt3
Path Finder

Dashboard search is named "export_download". I have a token that has that name in the dashboard as well (download_sid). I can get the token it by

    // Access the "default" token model
    var tokens = splunkjs.mvc.Components.get("default");

    // Retrieve the value of a token $mytoken$
    var download_sid = tokens.get("download_sid");
    var download_file_name = tokens.get("download_file_name");

    // get the search
    var exportSearch = splunkjs.mvc.Components.get(download_sid);

Now I have the search and can get the results just like before as the dashboard has already run it. I don't create the search manager. It is referencing the search in my dashboard.

0 Karma

pramit46
Contributor

Ohh okay. I see. you are actually using the result of the search which is run by the dashboard, instead of triggering it through JS. Swell!!!!

0 Karma

VatsalJagani
SplunkTrust
SplunkTrust

@hallt3 - could you please elaborate more on your question, like where you want download button? If you could give screenshot and/or UI mockup that would be great to understand your question.

0 Karma
Get Updates on the Splunk Community!

Extending Observability Content to Splunk Cloud

Register to join us !   In this Extending Observability Content to Splunk Cloud Tech Talk, you'll see how to ...

What's new in Splunk Cloud Platform 9.1.2312?

Hi Splunky people! We are excited to share the newest updates in Splunk Cloud Platform 9.1.2312! Analysts can ...

What’s New in Splunk Security Essentials 3.8.0?

Splunk Security Essentials (SSE) is an app that can amplify the power of your existing Splunk Cloud Platform, ...