<|start_filename|>connector/elasticsearchClient.js<|end_filename|>
var elasticsearchClient = (function () {

    var self = this;


    return {

    }

})();

console.log("[ElasticsearchClient]", elasticsearchClient);


<|start_filename|>package.json<|end_filename|>
{
  "name": "elasticsearch-tableau-connector",
  "version": "1.0.0",
  "description": "Tableau Web Data Connector for Elasticsearch",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node index.js",
    "install-service": "node_modules\\.bin\\winser -i -c",
    "uninstall-service": "node_modules\\.bin\\winser -r -x"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/mradamlacey/elasticsearch-tableau-connector.git"
  },
  "author": "@mradamlacey",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/mradamlacey/elasticsearch-tableau-connector/issues"
  },
  "homepage": "https://github.com/mradamlacey/elasticsearch-tableau-connector",
  "dependencies": {
    "express": "^4.13.3",
    "grunt": "^0.4.5",
    "grunt-contrib-watch": "^0.6.1",
    "grunt-express-server": "^0.5.1",
    "grunt-parallel": "^0.4.1",
    "load-grunt-tasks": "^3.2.0",
    "winser": "^1.0.2"
  },
  "devDependencies": {
    "grunt-contrib-clean": "^0.6.0",
    "grunt-contrib-copy": "^0.8.1",
    "grunt-contrib-cssmin": "^0.14.0",
    "grunt-contrib-uglify": "2.0.0",
    "grunt-targethtml": "^0.2.6"
  }
}


<|start_filename|>connector/elasticsearch-connector.css<|end_filename|>
        .code {
            width: 50%;
            white-space: pre-wrap;
            border: solid lightgrey 1px;
        }
        
        .tableau {
          display: none;
          margin-top: 25px;
        }
        
        .no-tableau {
          display: block;
        }
		
div.es-logo {
    display: block;
    height: 150px;
    width: 150px;
	background-image: url("elasticsearch-logo.png");
    background-repeat: no-repeat;
}

.title-row{
    background-color: #dddddd;
    -webkit-box-shadow: 0px 4px 17px -5px rgba(0,0,0,0.46);
-moz-box-shadow: 0px 4px 17px -5px rgba(0,0,0,0.46);
box-shadow: 0px 4px 17px -5px rgba(0,0,0,0.46);
}

.title{
    position: relative;
    top: 30px;

}

.title h2 {
    font-size: 40px;
}

.index-icon{
    position: relative;
    top: -24px;
    float: right;
    margin-right: 10px;
}

.type-icon{
    position: relative;
    top: -24px;
    float: right;
    margin-right: 10px;
}

        .icon-refresh-animate {
            animation-name: rotateThis;
            animation-duration: .75s;
            animation-iteration-count: infinite;
            animation-timing-function: linear;

            ms-animation-name: rotateThis;
            ms-animation-duration: .75s;
            ms-animation-iteration-count: infinite;
            ms-animation-timing-function: linear;
        }

        @keyframes rotateThis {
            from { transform: scale( 1 ) rotate( 0deg );   }
            to   { transform: scale( 1 ) rotate( 360deg ); }
        }

        @-ms-keyframes rotateThis{
            from { ms-transform: scale( 1 ) rotate( 0deg );   }
            to   { ms-transform: scale( 1 ) rotate( 360deg ); }
        }

        .hide{
            display: none;
        }

.query-editor{
    height: 200px;
    width: 100%;
    position: relative;

    border: 1px solid #ccc;
    border-radius: 4px;
    -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
    box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
    -webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;
    -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
    transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;

}

.query-editor:focus {
  border-color: #66afe9;
  outline: 0;
  -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6);
          box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6);
}

.date-range-number{
    width: 80px !important;
}

.agg-section{
    /* background-color: #0779A1;
    color: white; */
    margin-top: 25px;
}

.aggregation-builder-container{
    border-left: 5px solid #676767;
    padding-left: 10px;
}

.metrics-builder{
    border-color: #17A8E0;
}

.buckets-builder{
    border-color: #24BBB1;
}

#divSearchResultControls{
    border-left: 5px solid #FED10A;
    padding-left: 10px;
}

.range-row{
   padding: 5px;
   border-bottom: 1px solid #AAAAAA;
}

.alternaterow{
   /*
    background-color: #eeeeee;
    */
}

.typeahead.dropdown-menu{
    width: 100%;
}

.invalid-icon{
    color: red;
    font-size: large;
    top: 6px;
    margin: 2px;
}

/* Form Controls */
.form-group.required .control-label:after { 
   content:"*";
   color:red;
}

.input-group.input-group-unstyled input.form-control {
    -webkit-border-radius: 4px;
       -moz-border-radius: 4px;
            border-radius: 4px;
}
.input-group-unstyled .input-group-addon {
    border-radius: 4px;
    border: 0px;
    background-color: transparent;
}

.input-group-inline-icon{
    top: 10px;
}

.bucket-number-input{
    width: 120px !important;
}

.range-number-input{
    padding-right: 8px !important;
    width: 160px !important;
}

.range-type-select{
    width: 110px !important;
    padding-right: 14px !important;;
}

.card{
-webkit-box-shadow: -1px 3px 5px 2px rgba(0,0,0,0.18);
-moz-box-shadow: -1px 3px 5px 2px rgba(0,0,0,0.18);
box-shadow: -1px 3px 5px 2px rgba(0,0,0,0.18); 

    border: 1px solid #dddddd;

    -webkit-border-radius: 3px;
    -moz-border-radius: 3px;   
    border-radius: 3px;

    padding: 10px;
    margin-bottom: 10px;
}

.card.invalid{
    border-color: #843534;
    -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ce8483;
    box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ce8483;
}

.bucket_level_0{

}

.bucket_level_1{
    margin-left: 10px;
}

.bucket_level_2{
    margin-left: 20px;
}

.bucket_level_3{
    margin-left: 30px;
}

.bucket_level_4{
    margin-left: 40px;
}

.bucket_level_5{
    margin-left: 50px;
}

.bucket_level_6{
    margin-left: 60px;
}

.bucket_level_7{
    margin-left: 70px;
}

.bucket_level_8{
    margin-left: 80px;
}

.bucket_level_9{
    margin-left: 90px;
}

.bucket_level_10{
    margin-left: 100px;
}

#submitButton{
    margin-top: 25px;
    margin-bottom: 40px;
}

.preview-results{
   position: relative;
   margin-top: 10px;
}

.preview-loading{
    position: absolute;
    top: 0;
    left: 0;
    z-index: 100;
    background: rgba(255, 255, 255, .75);
    height: 400px;
    width: 100%;
}

.preview-text{
    font-size: larger;
    position: absolute;
    top: 50%;
    left: 20%;
}

/* Progress Bar */
.progress {
  position: absolute;
  top: 58%;
  left: 20%;
  width: 60%;

  height: 4px;
  display: block;

  background-color: rgba(102, 175, 233, .5);
  border-radius: 2px;
  background-clip: padding-box;
  margin: 0.5rem 0 1rem 0;
  overflow: hidden; }
  .progress .determinate {
    position: absolute;
    background-color: inherit;
    top: 0;
    bottom: 0;
    background-color: rgba(102, 175, 233, .9);
    transition: width .3s linear; }
  .progress .indeterminate {
    background-color: rgba(102, 175, 233, .9); }
    .progress .indeterminate:before {
      content: '';
      position: absolute;
      background-color: inherit;
      top: 0;
      left: 0;
      bottom: 0;
      will-change: left, right;
      -webkit-animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite;
              animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite; }
    .progress .indeterminate:after {
      content: '';
      position: absolute;
      background-color: inherit;
      top: 0;
      left: 0;
      bottom: 0;
      will-change: left, right;
      -webkit-animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite;
              animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite;
      -webkit-animation-delay: 1.15s;
              animation-delay: 1.15s; }

@-webkit-keyframes indeterminate {
  0% {
    left: -35%;
    right: 100%; }
  60% {
    left: 100%;
    right: -90%; }
  100% {
    left: 100%;
    right: -90%; } }
@keyframes indeterminate {
  0% {
    left: -35%;
    right: 100%; }
  60% {
    left: 100%;
    right: -90%; }
  100% {
    left: 100%;
    right: -90%; } }
@-webkit-keyframes indeterminate-short {
  0% {
    left: -200%;
    right: 100%; }
  60% {
    left: 107%;
    right: -8%; }
  100% {
    left: 107%;
    right: -8%; } }
@keyframes indeterminate-short {
  0% {
    left: -200%;
    right: 100%; }
  60% {
    left: 107%;
    right: -8%; }
  100% {
    left: 107%;
    right: -8%; } }

.table-container{
    padding: 10px;

        position: absolute;
    top: 0;
    left: 0;
    z-index: 10;

    -webkit-box-shadow: -1px 3px 5px 2px rgba(0,0,0,0.18);
-moz-box-shadow: -1px 3px 5px 2px rgba(0,0,0,0.18);
box-shadow: -1px 3px 5px 2px rgba(0,0,0,0.18); 

    border: 1px solid #dddddd;

    -webkit-border-radius: 3px;
    -moz-border-radius: 3px;   
    border-radius: 3px;
}

.table{
    font-size: small;
}
/*
thead, tbody, tr, td, th { display: block; }

tr:after {
    content: ' ';
    display: block;
    visibility: hidden;
    clear: both;
}

thead th {
    height: 30px;


}

tbody {
    height: 120px;
    overflow-y: auto;
}

thead {

}


tbody td, thead th {
    width: 19.2%;
    float: left;
}
*/

.sticky-header{
    position: fixed;
    top: 0px;
    left: 0px;
    background-color: red;
}

<|start_filename|>connector/app.js<|end_filename|>
var app = (function () {

    var AppViewModel = function () {

        var self = this;

        self.connectionName = ko.observable();
        self.useBasicAuthentication = ko.observable(false);
        self.useSyncClientWorkaround = ko.observable(false);
        self.username = ko.observable();
        self.password = ko.observable();
        self.elasticsearchUrl = ko.observable();
        self.elasticsearchIndex = ko.observable();
        self.elasticsearchAliasIndex = ko.observable();
        self.elasticsearchUnionAliasTypes = ko.observable(false);

        self.aliases = ko.observable([]);
        self.selectedAliasForIndex = ko.observable(false);

        self.overrideFieldDefaults = ko.observable(false);
        self.allDatesAsLocalTime = ko.observable(false);
        self.includeMilliseconds = ko.observable(true);
        self.useEsFieldNameAsAliases = ko.observable(false);
        self.useIncludeMilliseconds = ko.observable("When checked, this will include milliseconds when parsing date fields from Elasticsearch.  Ensure your type mapping supports milliseconds.");
        
        self.useEsFieldNameAsAliasesPopoverContent = ko.observable("When checked, this will override the default behavior and field names will be improved similar to how Tableau automatically improves field names of other data sources as defined in the following: <a href=\"http://onlinehelp.tableau.com/current/pro/desktop/en-us/help.htm#data_clean_adm.html\" target=\"_blank\">Tableau Help Article</a>.  Default when unchecked is to use the same names as in the Elasticsearch type.");

        self.resultMode = ko.observable("search");

        self.useCustomQuery = ko.observable(false);
        self.searchCustomQuery = ko.observable();

        self.useIncrementalRefresh = ko.observable(false);
        self.incrementalRefreshColumns = ko.observableArray([]);
        self.incrementalRefreshColumn = ko.observable();
        self.usingDateForIncrementalRefresh = ko.observable(false);
        self.incrementalRefreshColDateFormat = ko.observable();

        self.batchSize = ko.observable(10);
        self.limit = ko.observable(100);

        self.pauseSubscriptions = ko.observable(false);

        self.tableauInteractive = ko.observable(false);
        self.loaded = ko.observable(false);

        self.init = function(){
            elasticsearchConnector.subscribeInitEvent(function(connectionData){

                if (tableau.phase == tableau.phaseEnum.interactivePhase) {
                    console.log("[app] Connector UI called from Tableau WDC interactive mode, phase: ", tableau.phase)
                    self.tableauInteractive(true);
                }
                else if (!_.isNull(tableau.phase) && !_.isUndefined(tableau.phase)) {
                    console.log("[app] Connector UI called in phase: " + tableau.phase);
                }
                else{
                    console.log("[app] Connector UI called in standalone mode");
                }

                console.log("[app.init] Elasticsearch connector init fired!");
                self.setWithTableauConnectionData(connectionData);
                
                self.loaded(true);
            });
        };

        self.setWithTableauConnectionData = function (connectionData) {
            if (connectionData == null) {
                console.error("[app.setWithTableauConnectionData] [ERROR] - connectionData is null")
                return;
            }

            var tableauToVMMap = {
                "elasticsearchAuthenticate": "useBasicAuthentication",
                "elasticsearchUsername": "username",
                "elasticsearchPassword": "password",
                "elasticsearchQuery": "searchCustomQuery",
                "elasticsearchResultMode": "resultMode"
            };

            _.each(connectionData, function (val, key) {

                // Assume if we stored a key with the same name as we have on this ViewModel that it's a KO observable

                vmPropertyName = tableauToVMMap[key] ? tableauToVMMap[key] : key;

                if (_.isFunction(self[vmPropertyName])){

                    if (vmPropertyName == 'resultMode'){
                        return;
                    }

                    console.log("[app] - updating observable [" + vmPropertyName + "]", val);
                    self[vmPropertyName](val);
                }

            });

            if(connectionData != null){
                 vm.getElasticsearchFieldData(function(err, fieldData){
                     if(err){
                         return;
                     }
                      updateIncrementalRefreshColumns(err, fieldData);

                      vm.incrementalRefreshColumn(connectionData.incrementalRefreshColumn);
                 }, true);
            }

            if (connectionData != null && connectionData.elasticsearchAggregationData != null) {

                // Set Result mode and aggregation data separately
                self.pauseSubscriptions(true);

                console.log("Setting result mode to: " + connectionData.elasticsearchResultMode);
                self.resultMode(connectionData.elasticsearchResultMode);

                var aggData = connectionData.elasticsearchAggregationData;
                self.aggregations.pauseSubscriptions(true);
                console.log("Setting aggregations: ", aggData);

                self.aggregations.useCustomQuery(aggData.useCustomQuery)
                self.aggregations.aggregationFilter(aggData.filter)
                self.aggregations.useAggFilter(aggData.useAggFilter)
                self.aggregations.customQuery(aggData.customQuery)


                self.aggregations.getUpdatedTypeFields(function (err, data) {
                    if (err) {
                        console.error("[app] Error in getting aggregation type fields");
                        self.aggregations.pauseSubscriptions(false);
                        self.pauseSubscriptions(false);
                        return;

                    };

                    _.each(aggData.metrics, function (metric) {
                        var vmMetric = new self.aggregations.NewMetric(metric.type, metric.field);
                        self.aggregations.metrics.push(vmMetric);
                    });

                    _.each(aggData.buckets, function (bucket) {

                        var ranges = [];
                        var dateRanges = [];

                        _.each(bucket.ranges, function(range){
                             var vmRange = new self.aggregations.NewRange(range.type, range.from, range.to, range.relativeNumFrom, 
                                                                          range.relativeNumTo, range.fromRelative, range.toRelative, range.fromType, range.toType);
                            ranges.push(vmRange);
                        });
                        _.each(bucket.dateRanges, function(range){
                             var vmRange = new self.aggregations.NewRange(range.type, range.from, range.to, range.relativeNumFrom, 
                                                                          range.relativeNumTo, range.fromRelative, range.toRelative, range.fromType, range.toType);
                            dateRanges.push(vmRange);
                        });

                        var options = {
                            termSize: bucket.termSize,
                            dateHistogramType: bucket.dateHistogramType,
                            dateHistogramCustomInterval: bucket.dateHistogramCustomInterval,
                            ranges: ranges,
                            dateRanges: dateRanges
                        };

                        var vmBucket = new self.aggregations.NewBucket(bucket.type, bucket.field, options);
                        self.aggregations.buckets.push(vmBucket);
                    });
                });

                self.aggregations.pauseSubscriptions(false);
                self.pauseSubscriptions(false);
            }

            tableauData.updateProperties(vm.getTableauConnectionData());
        }

        self.elasticsearchIndexSource = function (something, cb) {

            $('.index-icon').toggleClass('hide');

            self.getElasticsearchIndices(function (err, indices) {

                if (err) {
                    $('.index-icon').toggleClass('hide');
                    return self.abort(err);
                }

                self.getElasticsearchAliases(function (err, aliases) {

                    $('.index-icon').toggleClass('hide');

                    if (err) {
                        return self.abort(err);
                    }
                    var uniqueAliasList = (_.uniq(aliases));
                    var sourceData = indices.concat(uniqueAliasList);

                    self.aliases(uniqueAliasList);

                    // Return the actual list of items to the control
                    cb(sourceData);
                });

            });
        };

        self.elasticsearchAliasIndexSource = function (something, cb) {

            $('.index-icon').toggleClass('hide');

            self.getElasticsearchRawAliases(function (err, aliases) {

                $('.index-icon').toggleClass('hide');

                if (err) {
                    return self.abort(err);
                }

                sourceData = [];

                _.forEach(aliases, function(indexAliases, indexName) {
                    if(indexAliases.aliases[self.elasticsearchIndex()]){
                        sourceData = sourceData.concat(indexName);
                    }
                });

                // Return the actual list of items to the control
                cb(sourceData);
            });
        };

        self.elasticsearchTypeSource = function (something, cb) {

            $('.type-icon').toggleClass('hide');

            var connectionData = tableauData.getUnwrapped();
            self.getElasticsearchTypes(connectionData.elasticsearchIndex, function (err, types) {
                $('.type-icon').toggleClass('hide');

                if (err) {
                    return self.abort(err);
                }

                // Return the actual list of items to the control
                cb(types);
            });
        };

        self.elasticsearchType = ko.observable();


        self.errorMessage = ko.observable();

        self.isErrorVisible = ko.computed(function () {
            return !self.errorMessage();
        });

        aggregations.setTableauData(tableauData.get());
        self.aggregations = aggregations;

        self.validation = ko.observable({});

        self.getTableauConnectionData = function () {

            var connectionData = {
                connectionName: self.connectionName(),
                elasticsearchUrl: self.elasticsearchUrl(),
                elasticsearchAuthenticate: self.useBasicAuthentication(),
                elasticsearchUsername: self.username(),
                elasticsearchPassword: self.password(),
                elasticsearchIndex: self.elasticsearchIndex(),
                elasticsearchAliasIndex: self.elasticsearchAliasIndex(),
                elasticsearchUnionAliasTypes: self.elasticsearchUnionAliasTypes(),
                elasticsearchType: self.elasticsearchType(),
                overrideFieldDefaults: self.overrideFieldDefaults(),
                allDatesAsLocalTime: self.allDatesAsLocalTime(),
                useEsFieldNameAsAliases: self.useEsFieldNameAsAliases(),
                useCustomQuery: self.useCustomQuery(),
                elasticsearchQuery: self.searchCustomQuery(),
                elasticsearchResultMode: self.resultMode(),
                elasticsearchAggregationData: aggregations.getAggregationData(),
                useSyncClientWorkaround: self.useSyncClientWorkaround(),
                useIncrementalRefresh: self.useIncrementalRefresh(),
                incrementalRefreshColumn: self.incrementalRefreshColumn(),
                includeMilliseconds: self.includeMilliseconds(),
                usingDateForIncrementalRefresh: self.usingDateForIncrementalRefresh(),
                incrementalRefreshColDateFormat: self.incrementalRefreshColDateFormat(),
                batchSize: self.batchSize(),
                limit: self.limit()
            };

            return connectionData;
        }

        self.isPreviewVisible = ko.observable(false);
        self.isPreviewDataLoading = ko.observable(false);
        self.previewFields = ko.observableArray([]);
        self.previewData = ko.observableArray([]);

        self.preview = function () {

            self.previewFields.removeAll();
            self.previewData.removeAll();

            self.isPreviewVisible(true);
            self.isPreviewDataLoading(true);

            setTimeout(function () {
                $('html, body').animate({
                    scrollTop: $("#preview-results").offset().top
                }, 250);
            }, 250);

            self.getElasticsearchFieldData(function (err, esFieldData) {

                if (err) {
                    self.isPreviewVisible(false);
                    self.isPreviewDataLoading(false);
                    return toastr.error(err);
                }

                _.each(esFieldData.fields, function (field) {
                    var connectionData = tableauData.getUnwrapped();
                    self.previewFields.push(elasticsearchConnector.getTableauFieldAlias(connectionData, field.name));
                });

                tableauData.updateProperties(esFieldData);

                if (self.resultMode() == "search") {
                    elasticsearchConnector.getSearchResponse(false, null, function (err, result) {
                        if (err) {
                            self.isPreviewVisible(false);
                            self.isPreviewDataLoading(false);
                            return toastr.error("Error getting preview data: " + err);
                        }

                        console.log("[App] - preview - opened search scroll window");

                        self.previewData(result.results);
                        self.isPreviewDataLoading(false);
                    });
                }

                if (self.resultMode() == "aggregation") {

                    elasticsearchConnector.getAggregationResponse(false, null, function (err, result) {

                        if (err) {
                            self.isPreviewVisible(false);
                            self.isPreviewDataLoading(false);
                            return toastr.error("Error getting preview data: " + err);
                        }

                        console.log("[App] - preview - received aggregation response");
                        self.previewData(result);

                        self.isPreviewDataLoading(false);
                    });
                }

            });


        };

        self.submit = function () {

            if(!self.tableauInteractive()){
                var err = "[app] [ERROR] Not in Tableau WDC interactive mode, returning...";
                return toastr.error(err);
            }

            self.getElasticsearchFieldData(function (err, esFieldData) {

                if (err) {
                    return self.abort(err, true);
                }

                tableauData.updateProperties(esFieldData);

                console.log("[App] Submitting tableau interactive phase data");
                tableau.submit();
            });

        }

        self.abort = function (errorMessage, kill) {

            self.errorMessage(errorMessage);

            toastr.error(errorMessage);

            console.error(errorMessage);
            if (kill) {
                tableau.abortWithError(errorMessage);
            }

        };

        self.getElasticsearchTypes = function (indexName, cb) {

            var connectionData = tableauData.getUnwrapped();

            if (!connectionData) {
                console.log("[App] getElasticsearchTypes - no connection data, nothing to do");
                return;
            }
            if (!connectionData.elasticsearchUrl || !connectionData.elasticsearchIndex) {
                console.log("[App] getElasticsearchTypes - no Elasticsearch URL or index, nothing to do");
                return;
            }

            var connectionUrl = connectionData.elasticsearchUrl.replace(/\/$/, "") + '/' + indexName + '/_mapping';

            var xhr = $.ajax({
                url: connectionUrl,
                method: 'GET',
                contentType: 'application/json',
                dataType: 'json',
                beforeSend: function (xhr) {
                    _beforeSendAddAuthHeader(xhr, connectionData);
                },
                success: function (data) {

                    self.errorMessage("");

                    var indices = _.keys(data);
                    var typeMap = {};

                    var esTypes = [];

                    _.each(indices, function (index) {

                        // If the index is an alias, then only return types part of the selected index alias filter
                        var isAlias = _.find(self.aliases(), function(alias){
                            return alias == self.elasticsearchIndex();
                        });
                        if(isAlias && index != self.elasticsearchAliasIndex()){
                            return;
                        }

                        var types = _.keys(data[index].mappings);
                        esTypes = esTypes.concat(types);
                    });

                    cb(null, esTypes);
                },
                error: function (xhr, ajaxOptions, err) {
                    if (xhr.status == 0) {
                        cb('Unable to get Elasticsearch types, unable to connect to host or CORS request was denied');
                    }
                    else {
                        cb("Unable to get Elasticsearch types, status code:  " + xhr.status + '; ' + xhr.responseText + "\n" + err);
                    }
                }
            });
        }

        self.getElasticsearchIndices = function (cb) {

            var connectionData = tableauData.getUnwrapped();

            if (!connectionData) {
                console.log("[App] getElasticsearchIndices - no connection data, nothing to do");
                return;
            }
            if (!connectionData.elasticsearchUrl) {
                console.log("[App] getElasticsearchIndices - no Elasticsearch URL, nothing to do");
                return;
            }

            var connectionUrl = connectionData.elasticsearchUrl.replace(/\/$/, "") + '/_mapping';

            var xhr = $.ajax({
                url: connectionUrl,
                method: 'GET',
                contentType: 'application/json',
                dataType: 'json',
                beforeSend: function (xhr) {
                    _beforeSendAddAuthHeader(xhr, connectionData);
                },
                success: function (data) {

                    self.errorMessage("");

                    var indices = _.keys(data);

                    cb(null, indices);
                },
                error: function (xhr, ajaxOptions, err) {
                    if (xhr.status == 0) {
                        cb('Unable to get Elasticsearch indices, unable to connect to host or CORS request was denied');
                    }
                    else {
                        cb("Unable to get Elasticsearch indices, status code:  " + xhr.status + '; ' + xhr.responseText + "\n" + err);
                    }
                }
            });
        }

        self.getElasticsearchAliases = function (cb) {

            var connectionData = tableauData.getUnwrapped();

            if (!connectionData) {
                console.log("[App] getElasticsearchIndices - no connection data, nothing to do");
                return;
            }
            if (!connectionData.elasticsearchUrl) {
                console.log("[App] getElasticsearchIndices - no Elasticsearch URL, nothing to do");
                return;
            }

            var connectionUrl = connectionData.elasticsearchUrl.replace(/\/$/, "") + '/_aliases';

            var xhr = $.ajax({
                url: connectionUrl,
                method: 'GET',
                contentType: 'application/json',
                dataType: 'json',
                beforeSend: function (xhr) {
                    _beforeSendAddAuthHeader(xhr, connectionData);
                },
                success: function (data) {

                    self.errorMessage("");

                    var aliasMap = {},
                        aliases = [];

                    _.forIn(data, function (value, key) {
                        aliases = aliases.concat(_.keys(value.aliases));
                    });

                    cb(null, aliases);
                },
                error: function (xhr, ajaxOptions, err) {
                    if (xhr.status == 0) {
                        cb('Unable to get Elasticsearch aliases, unable to connect to host or CORS request was denied');
                    }
                    else {
                        cb("Unable to get Elasticsearch aliases, status code:  " + xhr.status + '; ' + xhr.responseText + "\n" + err);
                    }
                }
            });
        };

        self.getElasticsearchRawAliases = function (cb) {
            
                        var connectionData = tableauData.getUnwrapped();
            
                        if (!connectionData) {
                            console.log("[App] getElasticsearchIndices - no connection data, nothing to do");
                            return;
                        }
                        if (!connectionData.elasticsearchUrl) {
                            console.log("[App] getElasticsearchIndices - no Elasticsearch URL, nothing to do");
                            return;
                        }
            
                        var connectionUrl = connectionData.elasticsearchUrl.replace(/\/$/, "") + '/_aliases';
            
                        var xhr = $.ajax({
                            url: connectionUrl,
                            method: 'GET',
                            contentType: 'application/json',
                            dataType: 'json',
                            beforeSend: function (xhr) {
                                _beforeSendAddAuthHeader(xhr, connectionData);
                            },
                            success: function (data) {
            
                                self.errorMessage("");
            
                                cb(null, data);
                            },
                            error: function (xhr, ajaxOptions, err) {
                                if (xhr.status == 0) {
                                    cb('Unable to get Elasticsearch aliases, unable to connect to host or CORS request was denied');
                                }
                                else {
                                    cb("Unable to get Elasticsearch aliases, status code:  " + xhr.status + '; ' + xhr.responseText + "\n" + err);
                                }
                            }
                        });
                    };

        self.getElasticsearchFieldData = function (cb, initialLoad) {
            var messages = [];

            var connectionData = tableauData.getUnwrapped();

            if (initialLoad === true) {
                if(connectionData == null){
                    return cb("Connection Data not provided for Elasticsearch", null);
                }
                if (!connectionData.elasticsearchUrl || !connectionData.elasticsearchIndex || !connectionData.elasticsearchType) {
                    return cb("Connection Data not provided for Elasticsearch", null);
                }
            }

            if (self.resultMode() == "aggregation") {
                var aggData = aggregations.getAggregationData();
                tableauData.updateProperties({ elasticsearchAggregationData: aggData });

                if (initialLoad === true) {
                    // Skip validation...
                }
                else {
                    var aggValidation = self.aggregations.validate();
                    messages = aggValidation.messages;
                }

            }

            if (initialLoad === true) {
                // Skip validation...
            }
            else {

                self.validate();

                if ((self.validation() && self.validation().messages.length > 0) ||
                    messages.length > 0) {

                    messages = messages.concat(self.validation().messages);

                    if (cb) cb(messages.join("<br />"));
                    return;
                }
            }

            // We have all the configuration filled for what data we want to retrieve from Elasticsearch
            // Go retrieve the actual fields and data types in order to update the Tableau connection data
            elasticsearchConnector.getElasticsearchConnectionFieldInfo(tableauData.getUnwrapped(), function (err, esFieldData) {
                if (err) {
                    console.log("[App] - error returned from getElasticsearchConnectionFieldInfo");
                    if (cb) cb(err);
                    return;
                }

                if (cb) {
                    cb(null, esFieldData)
                }
            });
        };

        self.validate = function () {

            var validation = {
                messages: []
            };

            if(!self.connectionName()){
                validation.messages.push("Connection name is required");
                validation.connectionName = true
            }

            if (self.useBasicAuthentication()) {
                if (!self.username()) {
                    validation.messages.push("Username is required");
                    validation.username = true
                }
                else {
                    validation.username = false;
                }

                if (!self.password()) {
                    validation.messages.push("Password is required");
                    validation.password = true
                }
                else {
                    validation.password = false;
                }
            }

            if (!self.elasticsearchUrl()) {
                validation.messages.push("Elasticsearch URL is required");
                validation.elasticsearchUrl = true
            }
            else {
                var isValid = isValidUrl(self.elasticsearchUrl());

                if (!isValid) validation.messages.push("Elasticsearch URL is not a valid URL");
                validation.elasticsearchUrl = !isValid;
            }

            if (!self.elasticsearchIndex()) {
                validation.messages.push("Elasticsearch Index is required");
                validation.elasticsearchIndex = true
            }
            else {
                validation.elasticsearchIndex = false;
            }

            var isAlias = _.find(self.aliases(), function(alias){
                return alias == self.elasticsearchIndex();
            });
            
            if (isAlias && !self.elasticsearchAliasIndex()) {
                validation.messages.push("Elasticsearch Alias Index is required");
                validation.elasticsearchAliasIndex = true
            }
            else {
                validation.elasticsearchAliasIndex = false;
            }

            if (!self.elasticsearchType()) {
                validation.messages.push("Elasticsearch Type is required");
                validation.elasticsearchType = true
            }
            else {
                validation.elasticsearchType = false;
            }

            if (self.useCustomQuery()) {
                if (!self.searchCustomQuery()) {
                    validation.messages.push("Custom query is required");
                    validation.searchCustomQuery = true;
                }
                else {
                    validation.searchCustomQuery = false;
                }
            }

            self.validation(validation);
        };

    };

    var isValidUrl = function (str) {
        var pattern = new RegExp('^(https?:\/\/)?' + // protocol
            '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\.)+[a-z\\d]{2,}|' + // domain name
            '((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address
            '(\\:\\d+)?(\/[-a-z\\d%_.~+]*)*' + // port and path
            '(\\?[;&a-z\\d%_.~+=-]*)?' + // query string
            '(\\#[-a-z\\d_]*)?$', 'i'); // fragment locater
        if (!pattern.test(str)) {
            return false;
        }
        else {
            return true;
        }
    }

    var _getAuthCredentials = function (connectionData) {

        if (connectionData.useSyncClientWorkaround) {
            return {
                username: connectionData.elasticsearchUsername,
                password: connectionData.elasticsearchPassword
            };
        }
        else {
            return {
                username: tableau.username,
                password: <PASSWORD>
            };
        }
    };

    var _beforeSendAddAuthHeader = function (xhr, connectionData) {

        var creds = _getAuthCredentials(connectionData);

        if (connectionData.elasticsearchAuthenticate && creds.username) {
            xhr.setRequestHeader("Authorization", "Basic " + btoa(creds.username + ":" + creds.password));
        }
    };

    var vm = new AppViewModel();

    ///////////////////////////////////////////////////////////////////////////////////////////
    // Subscribe to changes in the VM and keep the tableau properties in sync
    ///////////////////////////////////////////////////////////////////////////////////////////
    vm.useBasicAuthentication.subscribe(function (newValue) {

        if (newValue === false) {
            vm.username("");
            vm.password("");
            vm.useSyncClientWorkaround(false);
        }

        tableauData.updateAuthCredentials(vm.useBasicAuthentication(), vm.useSyncClientWorkaround(), vm.username(), vm.password());
    });

    vm.username.subscribe(function (newValue) {
        tableauData.updateAuthCredentials(vm.useBasicAuthentication(), vm.useSyncClientWorkaround(), vm.username(), vm.password());
    });

    vm.password.subscribe(function (newValue) {
        tableauData.updateAuthCredentials(vm.useBasicAuthentication(), vm.useSyncClientWorkaround(), vm.username(), vm.password());
    });

    vm.useSyncClientWorkaround.subscribe(function (newValue) {
        tableauData.updateAuthCredentials(vm.useBasicAuthentication(), vm.useSyncClientWorkaround(), vm.username(), vm.password());
    });

    var updateIncrementalRefreshColumns = function(err, fieldData){

        var incrementalRefreshColumn = vm.incrementalRefreshColumn();

        if(err){
            console.error("[app] Unable to refresh incremental refresh fields");
            return;
        }

        console.log("[app] Getting elasticsearch incremental refresh fields...", fieldData);

        vm.incrementalRefreshColumns.removeAll();
        _.each(fieldData.fields, function(field){
            if(field.name == "_id" || field.name == "_sequence"){
                return;
            }
            
            vm.incrementalRefreshColumns.push(field);
        });

        console.log("[app] Resetting incremental refresh column value to: " + incrementalRefreshColumn);

        if(incrementalRefreshColumn){
            vm.incrementalRefreshColumn(incrementalRefreshColumn);
        }
    };

    vm.connectionName.subscribe(function (newValue) {

        tableauData.updateProperties(vm.getTableauConnectionData());
        vm.validate();

    });

    vm.elasticsearchUrl.subscribe(function (newValue) {

        vm.elasticsearchIndex("");
        vm.elasticsearchAliasIndex("");
        vm.elasticsearchUnionAliasTypes(false);
        vm.elasticsearchType("");

        vm.previewFields.removeAll();
        vm.previewData.removeAll();

        vm.aggregations.clear();
        tableauData.updateProperties(vm.getTableauConnectionData());

        vm.getElasticsearchFieldData(updateIncrementalRefreshColumns);
    });

    vm.elasticsearchIndex.subscribe(function (newValue) {

        if (vm.aliases().length == 0) {
            vm.getElasticsearchAliases(function (err, aliases) {
                var uniqueAliasList = (_.uniq(aliases));
                vm.aliases(uniqueAliasList);

                var isAlias = _.find(vm.aliases(), function(alias){
                    return alias == newValue;
                });
                vm.selectedAliasForIndex(isAlias ? true : false);
            });
        }
        else{
            var isAlias = _.find(vm.aliases(), function(alias){
                return alias == newValue;
            });
            vm.selectedAliasForIndex(isAlias ? true : false);
        }

        vm.elasticsearchAliasIndex("");
        vm.elasticsearchUnionAliasTypes(false);
        vm.elasticsearchType("");

        vm.previewFields.removeAll();
        vm.previewData.removeAll();

        vm.aggregations.clear();
        tableauData.updateProperties(vm.getTableauConnectionData());

        vm.getElasticsearchFieldData(updateIncrementalRefreshColumns);
    });

    vm.selectedAliasForIndex.subscribe(function(newValue){

        // If not using an alias for the index - then duplicate the value
        if(!newValue){
            vm.elasticsearchAliasIndex(vm.elasticsearchIndex());
        }
    });

    vm.elasticsearchAliasIndex.subscribe(function (newValue) {

        vm.elasticsearchType("");

        vm.previewFields.removeAll();
        vm.previewData.removeAll();

        vm.aggregations.clear();
        tableauData.updateProperties(vm.getTableauConnectionData());

        vm.getElasticsearchFieldData(updateIncrementalRefreshColumns);
    });

    vm.elasticsearchUnionAliasTypes.subscribe(function (newValue) {

        vm.previewFields.removeAll();
        vm.previewData.removeAll();

        vm.aggregations.clear();
        tableauData.updateProperties(vm.getTableauConnectionData());

        vm.getElasticsearchFieldData(updateIncrementalRefreshColumns);
    });

    vm.elasticsearchType.subscribe(function (newValue) {
        vm.previewFields.removeAll();
        vm.previewData.removeAll();

        vm.aggregations.clear();
        tableauData.updateProperties(vm.getTableauConnectionData());

        vm.getElasticsearchFieldData(updateIncrementalRefreshColumns);
    });

    vm.overrideFieldDefaults.subscribe(function (newValue) {
        if(!newValue){
            vm.allDatesAsLocalTime(false);
            vm.useEsFieldNameAsAliases(false);
        }
        tableauData.updateProperties(vm.getTableauConnectionData());
    });
    vm.allDatesAsLocalTime.subscribe(function (newValue) {
        tableauData.updateProperties(vm.getTableauConnectionData());
    });
    vm.useEsFieldNameAsAliases.subscribe(function (newValue) {
        tableauData.updateProperties(vm.getTableauConnectionData());
    });

    vm.useCustomQuery.subscribe(function(newValue){
        if( newValue == false){
            console.log("Setting use custom query to false...")
            vm.searchCustomQuery();
        }
        else{
            tableauData.updateProperties(vm.getTableauConnectionData());
        }  
    });

    vm.searchCustomQuery.subscribe(function (newValue) {
        vm.previewFields.removeAll();
        vm.previewData.removeAll();

        tableauData.updateProperties(vm.getTableauConnectionData());
    });

    vm.useIncrementalRefresh.subscribe(function (newValue) {
        tableauData.updateProperties(vm.getTableauConnectionData());
    });

    vm.incrementalRefreshColumn.subscribe(function (newValue) {

        var esDateFields = elasticsearchConnector.getElasticsearchDateFields();

        var connectionData = vm.getTableauConnectionData();
        tableauData.updateProperties(connectionData);

        var isDateField = _.find(esDateFields, function(field){
            if (field == newValue){
                return true;
            }

            return false;
        });
        if(isDateField){
            vm.usingDateForIncrementalRefresh(true);
        }
        else{
            vm.usingDateForIncrementalRefresh(false);
        }
    });

    vm.batchSize.subscribe(function (newValue) {
        tableauData.updateProperties(vm.getTableauConnectionData());
    });

    vm.limit.subscribe(function (newValue) {
        tableauData.updateProperties(vm.getTableauConnectionData());
    });

    vm.resultMode.subscribe(function (newValue) {
        if(vm.pauseSubscriptions()){
            return;
        }

        console.log("[App] resultMode changed: " + newValue);

        vm.previewFields.removeAll();
        vm.previewData.removeAll();
        vm.isPreviewVisible(false);

        vm.aggregations.clear();
        tableauData.updateProperties(vm.getTableauConnectionData());
    });

    //    vm.aggregations.getAggregationData().subscribe(function(newValue){
    //        console.log("[App] aggregations changed: " + newValue);
    //    });

    ///////////////////////////////////////////////////////////////////////////////////////////
    ///////////////////////////////////////////////////////////////////////////////////////////

    ko.applyBindings(vm);

    toastr.options.positionClass = "toast-top-center";
    toastr.options.preventDuplicates = true;

    return vm;

})();

console.log("[App]", app);
app.init();


<|start_filename|>docker/Dockerfile<|end_filename|>
FROM ubuntu:14.04

RUN apt-get update && \
	apt-get install -y \
							curl \
							git

RUN curl -sL https://deb.nodesource.com/setup | bash -

RUN apt-get install -y nodejs

RUN npm install -g grunt-cli bower && \
	git config --global url."https://".insteadOf git://

RUN cd /usr/local && \
	git clone --depth=1 --branch=master https://github.com/mradamlacey/elasticsearch-tableau-connector.git && \
	cd elasticsearch-tableau-connector && \
	rm -rf .git && \
	npm install && \
	bower install --allow-root && \
	grunt build:dev

WORKDIR "/usr/local/elasticsearch-tableau-connector"

EXPOSE 3000

CMD ["/usr/bin/nodejs", "index.js"]


<|start_filename|>connector/knockoutCustomBindings.js<|end_filename|>
var koCustomBindings = (function () {

    // Bootstrap DatePicker
    ko.bindingHandlers.bootstrapDatePicker = {
        init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
            $(element).datepicker({
               todayBtn: "linked"
            });
        }
    };

    ko.bindingHandlers.bootstrapPopover = {
     init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {

            var bootstrapPopoverType = allBindings.get('bootstrapPopoverType') ? allBindings.get('bootstrapPopoverType') : 'default',
                bootstrapPopoverContent = allBindings.get('bootstrapPopoverContent') ? allBindings.get('bootstrapPopoverContent') : '';

            if (bootstrapPopoverType == 'default') {
                $(element).popover({
                    container: "body",
                    trigger: "hover"
                });
            }
            else {
                $(element).popover({
                    container: "body",
                    trigger: "hover",
                    html: true,
                    delay: { hide: 1200 },
                    placement: "left",
                    content: bootstrapPopoverContent
                });
            }


        }   
    };

    ko.bindingHandlers.typeahead = {
        init: function(element, valueAccessor, allBindings, viewModel, bindingContext){

            var value = valueAccessor();
            var valueUnwrapped = ko.unwrap(value);

            var source = allBindings.get('typeaheadSource');

            $(element).typeahead({
                source: source,
                autoSelect: true,
                showHintOnFocus: true,
                items: 'all',
                fitToElement: true
            });

        }
    };

    ko.bindingHandlers.aceEditor = {
        init: function(element, valueAccessor, allBindings, viewModel, bindingContext){

            var value = valueAccessor();
            var valueUnwrapped = ko.unwrap(value);

            var aceEditorText = allBindings.get('aceEditorText');
            var aceEditorTheme = allBindings.get('aceEditorTheme');   // e.g. 'ace/theme/github'
            var aceEditorLanguage = allBindings.get('aceEditorLanguage');   // e.g. 'ace/mode/json'

            var queryEditor = ace.edit(element);
            queryEditor.setTheme(aceEditorTheme ? aceEditorTheme : "ace/theme/github");
            queryEditor.getSession().setMode(aceEditorLanguage ? aceEditorLanguage : "ace/mode/json");

            if (aceEditorText) {

                if (ko.isObservable(aceEditorText)) {
                    aceEditorText.subscribe(function(changedData){

                        var currentValue = queryEditor.getValue();
                        if(_.trim(currentValue) == _.trim(changedData)){
                            return;
                        }

                        queryEditor.setValue(changedData);
                    });
                }

                queryEditor.on("change", function (changedData) {
                    if (ko.isObservable(aceEditorText)) {
                        aceEditorText(queryEditor.getValue());
                    }
                });

            }
        }
    };    

    ko.bindingHandlers.bootstrapChecked = {
        init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
            var value = valueAccessor();
            var newValueAccessor = function () {
                return {
                    change: function () {
                        value(element.value);
                    }
                }
            };

            console.log("Element val: " + $(element).val() + ", val accessor: " + ko.unwrap(valueAccessor()));

            if ($(element).val() == ko.unwrap(valueAccessor())) {
                $(element).closest('.btn').button('toggle');
            }
            ko.bindingHandlers.event.init(element, newValueAccessor, allBindingsAccessor, viewModel, bindingContext);
        },
        update: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
            console.log("Element val: " + $(element).val() + ", val accessor: " + ko.unwrap(valueAccessor()));
            if ($(element).val() == ko.unwrap(valueAccessor())) {
                $(element).closest('.btn').button('toggle');
            }
        }
    };

    ko.bindingHandlers.stickyHeader = {

        init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {

            // Cache selectors for faster performance.
            var $window = $(window),
                $element = $(element),
                stickyHeaderAnchorElementId = allBindings.get('stickyHeaderAnchorElementId');

            $anchorElement = $('#' + stickyHeaderAnchorElementId);

            // Run this on scroll events.
            $window.scroll(function () {
                var window_top = $window.scrollTop();
                var div_top = $anchorElement.offset().top;
                if (window_top > div_top) {
                    // Make the div sticky.
                    $element.addClass('sticky-header');
                    $anchorElement.height($element.height());
                }
                else {
                    // Unstick the div.
                    $element.removeClass('sticky-header');
                    $anchorElement.height(0);
                }
            });

        }
    };
    

    return {
        bootstrapDatePicker: ko.bindingHandlers.bootstrapDatePicker,
        bootstrapPopover: ko.bindingHandlers.bootstrapPopover,
        typeahead: ko.bindingHandlers.typeahead,
        aceEditor: ko.bindingHandlers.aceEditor 
    };

})();

console.log("[KnockoutCustomBindings]", koCustomBindings);
