ambari-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From alexantone...@apache.org
Subject ambari git commit: AMBARI-13313. Ability to export graphs as CSV
Date Mon, 05 Oct 2015 17:18:45 GMT
Repository: ambari
Updated Branches:
  refs/heads/branch-2.1 2bf590caf -> 3737010b0


AMBARI-13313. Ability to export graphs as CSV


Project: http://git-wip-us.apache.org/repos/asf/ambari/repo
Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/3737010b
Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/3737010b
Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/3737010b

Branch: refs/heads/branch-2.1
Commit: 3737010b0e1aab1ca01d4bd41ce261beecee9480
Parents: 2bf590c
Author: Alex Antonenko <hiveww@gmail.com>
Authored: Mon Oct 5 20:12:14 2015 +0300
Committer: Alex Antonenko <hiveww@gmail.com>
Committed: Mon Oct 5 20:18:16 2015 +0300

----------------------------------------------------------------------
 ambari-web/app/assets/test/tests.js             |   1 +
 .../main/admin/kerberos/step5_controller.js     |  47 +---
 ambari-web/app/messages.js                      |   4 +
 ambari-web/app/mixins.js                        |   1 +
 .../common/widgets/export_metrics_mixin.js      | 119 +++++++++
 .../app/mixins/common/widgets/widget_mixin.js   |   1 +
 ambari-web/app/styles/application.less          | 111 +++++---
 ambari-web/app/styles/common.less               |   7 +
 .../app/styles/enhanced_service_dashboard.less  | 105 ++++----
 .../app/templates/common/chart/linear_time.hbs  |  13 +-
 .../templates/common/widget/graph_widget.hbs    |   7 +
 .../app/templates/main/charts/linear_time.hbs   |  14 +-
 .../main/dashboard/widgets/cluster_metrics.hbs  |  11 +
 ambari-web/app/utils/file_utils.js              |  79 ++++++
 .../app/views/common/chart/linear_time.js       |  59 ++++-
 .../views/common/widget/graph_widget_view.js    |  24 +-
 .../dashboard/widgets/cluster_metrics_widget.js |  27 +-
 .../common/widgets/export_metrics_mixin_test.js | 263 +++++++++++++++++++
 18 files changed, 756 insertions(+), 137 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/3737010b/ambari-web/app/assets/test/tests.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/assets/test/tests.js b/ambari-web/app/assets/test/tests.js
index 432e6fd..2689808 100644
--- a/ambari-web/app/assets/test/tests.js
+++ b/ambari-web/app/assets/test/tests.js
@@ -142,6 +142,7 @@ var files = ['test/init_model_test',
   'test/mixins/common/configs/configs_saver_test',
   'test/mixins/common/configs/toggle_isrequired_test',
   'test/mixins/common/chart/storm_linear_time_test',
+  'test/mixins/common/widgets/export_metrics_mixin_test',
   'test/mixins/common/widgets/widget_section_test',
   'test/mixins/common/localStorage_test',
   'test/mixins/common/reload_popup_test',

http://git-wip-us.apache.org/repos/asf/ambari/blob/3737010b/ambari-web/app/controllers/main/admin/kerberos/step5_controller.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/controllers/main/admin/kerberos/step5_controller.js b/ambari-web/app/controllers/main/admin/kerberos/step5_controller.js
index 3d16f84..f2b469c 100644
--- a/ambari-web/app/controllers/main/admin/kerberos/step5_controller.js
+++ b/ambari-web/app/controllers/main/admin/kerberos/step5_controller.js
@@ -15,7 +15,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
- var stringUtils = require('utils/string_utils');
+var stringUtils = require('utils/string_utils');
+var fileUtils = require('utils/file_utils');
 
 App.KerberosWizardStep5Controller = App.KerberosProgressPageController.extend({
   name: 'kerberosWizardStep5Controller',
@@ -46,7 +47,7 @@ App.KerberosWizardStep5Controller = App.KerberosProgressPageController.extend({
   getCSVDataSuccessCallback: function (data, opt, params) {
     this.set('csvData', this.prepareCSVData(data.split('\n')));
     if(!Em.get(params, 'skipDownload')){
-      this.downloadCSV();
+      fileUtils.downloadTextFile(stringUtils.arrayToCSV(this.get('csvData')), 'csv', 'kerberos.csv');
     }
   },
 
@@ -59,48 +60,6 @@ App.KerberosWizardStep5Controller = App.KerberosProgressPageController.extend({
   },
 
   /**
-   * download CSV file
-   */
-  downloadCSV: function () {
-    if ($.browser.msie && $.browser.version < 10) {
-      this.openInfoInNewTab();
-    } else if (typeof safari !== 'undefined') {
-      this.safariDownload();
-    } else {
-      try {
-        var blob = new Blob([stringUtils.arrayToCSV(this.get('csvData'))], {type: "text/csv;charset=utf-8;"});
-        saveAs(blob, "kerberos.csv");
-      } catch (e) {
-        this.openInfoInNewTab();
-      }
-    }
-  },
-
-  /**
-   * Hack to dowload csv data in Safari
-   */
-  safariDownload: function() {
-    var file = 'data:attachment/csv;charset=utf-8,' + encodeURI(stringUtils.arrayToCSV(this.get('csvData')));
-    var linkEl = document.createElement("a");
-    linkEl.href = file;
-    linkEl.download = 'kerberos.csv';
-
-    document.body.appendChild(linkEl);
-    linkEl.click();
-    document.body.removeChild(linkEl);
-  },
-
-  /**
-   * open content of CSV file in new window
-   */
-  openInfoInNewTab: function () {
-    var newWindow = window.open('');
-    var newDocument = newWindow.document;
-    newDocument.write(stringUtils.arrayToCSV(this.get('hostComponents')));
-    newWindow.focus();
-  },
-
-  /**
    * Send request to post kerberos descriptor
    * @param kerberosDescriptor
    * @returns {$.ajax|*}

http://git-wip-us.apache.org/repos/asf/ambari/blob/3737010b/ambari-web/app/messages.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/messages.js b/ambari-web/app/messages.js
index 0570c5b..279b40d 100644
--- a/ambari-web/app/messages.js
+++ b/ambari-web/app/messages.js
@@ -272,6 +272,10 @@ Em.I18n.translations = {
   'common.removed': 'Removed',
   'common.testing': 'Testing',
   'common.noData': 'No Data',
+  'common.export': 'Export',
+  'common.csv': 'CSV',
+  'common.json': 'JSON',
+  'common.timestamp': 'Timestamp',
   'common.loading.eclipses': 'Loading...',
 
   'models.alert_instance.tiggered.verbose': "Occurred on {0} <br> Checked on {1}",

http://git-wip-us.apache.org/repos/asf/ambari/blob/3737010b/ambari-web/app/mixins.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/mixins.js b/ambari-web/app/mixins.js
index 6552c61..f5000eb 100644
--- a/ambari-web/app/mixins.js
+++ b/ambari-web/app/mixins.js
@@ -49,6 +49,7 @@ require('mixins/common/configs/configs_saver');
 require('mixins/common/configs/configs_loader');
 require('mixins/common/configs/configs_comparator');
 require('mixins/common/configs/toggle_isrequired');
+require('mixins/common/widgets/export_metrics_mixin');
 require('mixins/common/widgets/widget_mixin');
 require('mixins/common/widgets/widget_section');
 require('mixins/unit_convert/base_unit_convert_mixin');

http://git-wip-us.apache.org/repos/asf/ambari/blob/3737010b/ambari-web/app/mixins/common/widgets/export_metrics_mixin.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/mixins/common/widgets/export_metrics_mixin.js b/ambari-web/app/mixins/common/widgets/export_metrics_mixin.js
new file mode 100644
index 0000000..395557e
--- /dev/null
+++ b/ambari-web/app/mixins/common/widgets/export_metrics_mixin.js
@@ -0,0 +1,119 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var App = require('app');
+
+var stringUtils = require('utils/string_utils');
+var fileUtils = require('utils/file_utils');
+
+App.ExportMetricsMixin = Em.Mixin.create({
+
+  /**
+   * Used as argument passed from template to indicate that resulting format is CSV, not
JSON
+   */
+  exportToCSVArgument: true,
+
+  toggleFormatsList: function () {
+    this.$('.export-graph-list').toggle();
+  },
+
+  exportGraphData: function () {
+    this.toggleFormatsList();
+  },
+
+  exportGraphDataSuccessCallback: function (response, request, params) {
+    var hasData = response && response.metrics && Em.keys(response.metrics).length;
+    if (!hasData) {
+      App.showAlertPopup(Em.I18n.t('graphs.noData.title'), Em.I18n.t('graphs.noData.tooltip.title'));
+    } else {
+      var fileType = params.isCSV ? 'csv' : 'json',
+        fileName = 'data.' + fileType,
+        data = params.isCSV ? this.prepareCSV(response) : this.prepareJSON(response);
+      fileUtils.downloadTextFile(data, fileType, fileName);
+    }
+  },
+
+  exportGraphDataErrorCallback: function (jqXHR, ajaxOptions, error, opt) {
+    App.ajax.defaultErrorHandler(jqXHR, opt.url, opt.method, jqXHR.status);
+  },
+
+  /**
+   * Take metrics from any depth level in JSON response
+   * @method setMetricsArrays
+   * @param data
+   * @param metrics
+   * @param titles
+   */
+  setMetricsArrays: function (data, metrics, titles) {
+    Em.keys(data).forEach(function (key) {
+      if (Em.isArray(data[key])) {
+        titles.push(key);
+        metrics.push(data[key]);
+      } else {
+        this.setMetricsArrays(data[key], metrics, titles);
+      }
+    }, this);
+  },
+
+  prepareCSV: function (data) {
+    var metrics = [],
+      getMetricsItem = function (i, j, k) {
+        var item;
+        if (data.metrics) {
+          item = metrics[j][i][k];
+        } else if (Em.isArray(data)) {
+          item = data[j].data[i][k];
+        }
+        return item;
+      },
+      titles,
+      ticksNumber,
+      metricsNumber,
+      metricsArray;
+    if (data.metrics) {
+      titles = [Em.I18n.t('common.timestamp')];
+      this.setMetricsArrays(data.metrics, metrics, titles);
+      ticksNumber = metrics[0].length;
+      metricsNumber = metrics.length
+    } else if (Em.isArray(data)) {
+      titles = data.mapProperty('name');
+      titles.unshift(Em.I18n.t('common.timestamp'));
+      ticksNumber = data[0].data.length;
+      metricsNumber = data.length;
+    }
+    metricsArray = [titles];
+    for (var i = 0; i < ticksNumber; i++) {
+      metricsArray.push([getMetricsItem(i, 0, 1)]);
+      for (var j = 0; j < metricsNumber; j++) {
+        metricsArray[i + 1].push(getMetricsItem(i, j, 0));
+      };
+    }
+    return stringUtils.arrayToCSV(metricsArray);
+  },
+
+  prepareJSON: function (data) {
+    var fileData;
+    if (data.metrics) {
+      fileData = JSON.stringify(data.metrics, null, 4);
+    } else if (Em.isArray(data)) {
+      fileData = JSON.stringify(data, ['name', 'data'], 4);
+    }
+    return fileData;
+  }
+
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/3737010b/ambari-web/app/mixins/common/widgets/widget_mixin.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/mixins/common/widgets/widget_mixin.js b/ambari-web/app/mixins/common/widgets/widget_mixin.js
index d003949..c639c77 100644
--- a/ambari-web/app/mixins/common/widgets/widget_mixin.js
+++ b/ambari-web/app/mixins/common/widgets/widget_mixin.js
@@ -374,6 +374,7 @@ App.WidgetMixin = Ember.Mixin.create({
       Em.run.next(function(){
         App.tooltip(self.$(".corner-icon > .icon-copy"), {title: Em.I18n.t('common.clone')});
         App.tooltip(self.$(".corner-icon > .icon-edit"), {title: Em.I18n.t('common.edit')});
+        App.tooltip(self.$(".corner-icon > .icon-save"), {title: Em.I18n.t('common.export')});
       });
     }
   }.observes('isLoaded'),

http://git-wip-us.apache.org/repos/asf/ambari/blob/3737010b/ambari-web/app/styles/application.less
----------------------------------------------------------------------
diff --git a/ambari-web/app/styles/application.less b/ambari-web/app/styles/application.less
index d010bc1..0b59119 100644
--- a/ambari-web/app/styles/application.less
+++ b/ambari-web/app/styles/application.less
@@ -2177,6 +2177,16 @@ a:focus {
   .modal-body {
     min-height: 420px !important;
     overflow: hidden;
+    .corner-icon {
+      text-decoration: none;
+      .icon-save {
+        color: #555;
+      }
+    }
+    .export-graph-list {
+      top: auto;
+      right: 15px;
+    }
   }
 }
 
@@ -2299,9 +2309,31 @@ a:focus {
     z-index: 5;
   }
   .chart-title {
+    padding-right: 15px;
     text-align: center;
     font-size: small;
   }
+  .corner-icon {
+    position: absolute;
+    right: 0;
+    bottom: -10px;
+    text-decoration: none;
+    i {
+      color: #555;
+    }
+  }
+  .export-graph-list-top {
+    position: absolute;
+    bottom: -5px;
+    display: block;
+    width: 100%;
+    height: 10px;
+  }
+  .export-graph-list {
+    top: auto;
+    right: -1px;
+    bottom: -65px;
+  }
 }
 
 .modal-body {
@@ -2649,10 +2681,17 @@ table.graphs {
         left: -13px;
         top: -10px;
       }
-      .icon-edit{
+      .icon-edit, .icon-save {
         color: #555555;
       }
     }
+    .export-graph-list {
+      right: 3px;
+      li {
+        margin: 0;
+        height: auto;
+      }
+    }
     .thumbnail .hidden-info-general{
       color: #555555;
       font-size: 12px;
@@ -2792,47 +2831,49 @@ table.graphs {
       }
     }
 
-    .cluster-metrics .chart-container{
-      margin: 0px 10px 0px 10px;
-      .chart-y-axis{
-        margin-top: 10px;
+    .cluster-metrics {
+      position: relative;
+      .chart-container{
+        margin: 0px 10px 0px 10px;
+        .chart-y-axis{
+          margin-top: 10px;
+        }
+        .chart svg{
+          margin-right: 20px;
+        }
+        .rickshaw_legend{
+          padding-top: 3px;
+        }
+        .chart-legend {
+          top: 120px;
+          left:15px;
+          text-align: left;
+          z-index: 3;
+          ul >li{
+            max-height: 10px;
+          }
+        }
       }
-      .chart svg{
-        margin-right: 20px;
+      &> ul {
+        margin:0;
       }
-      .rickshaw_legend{
-        padding-top: 3px;
+      .alert {
+        padding: 0px;
+        font-size: 12px;
       }
-      .chart-legend {
-        top: 120px;
-        left:15px;
-        text-align: left;
-        z-index: 3;
-        ul >li{
-          max-height: 10px;
+      .thumbnail:hover {
+        cursor: move;
+        .corner-icon {
+          display:block;
+          text-decoration: none;
+          z-index: 9;
+        }
+        .caption {
+          margin-left: -6px;
         }
       }
     }
 
-    .cluster-metrics > ul {
-      margin:0;
-    }
-
-    .cluster-metrics .alert {
-      padding: 0px;
-      font-size: 12px;
-    }
-    .cluster-metrics .thumbnail:hover{
-      cursor: move;
-      .corner-icon{
-        display:block;
-        text-decoration: none;
-        z-index: 9;
-      }
-      .caption{
-        margin-left: -6px;
-      }
-    }
     .links {
       ul {
         margin: 0;

http://git-wip-us.apache.org/repos/asf/ambari/blob/3737010b/ambari-web/app/styles/common.less
----------------------------------------------------------------------
diff --git a/ambari-web/app/styles/common.less b/ambari-web/app/styles/common.less
index e828653..6559c06 100644
--- a/ambari-web/app/styles/common.less
+++ b/ambari-web/app/styles/common.less
@@ -348,4 +348,11 @@
       padding: 0;
     }
   }
+}
+
+.export-graph-list {
+  top: 25px;
+  min-width: 60px;
+  font-size: 14px;
+  cursor: default;
 }
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ambari/blob/3737010b/ambari-web/app/styles/enhanced_service_dashboard.less
----------------------------------------------------------------------
diff --git a/ambari-web/app/styles/enhanced_service_dashboard.less b/ambari-web/app/styles/enhanced_service_dashboard.less
index f6f17ea..c1a241c 100644
--- a/ambari-web/app/styles/enhanced_service_dashboard.less
+++ b/ambari-web/app/styles/enhanced_service_dashboard.less
@@ -59,6 +59,9 @@
     .chart-container .alert {
       margin-bottom: 5px;
     }
+    .export-graph-list {
+      right: -5px;
+    }
   }
 }
 
@@ -202,59 +205,67 @@
 
 #widget_layout {
   .widget {
-    .thumbnail .corner-icon {
-      display: none;
-      .icon-remove-sign{
-        color: #000000;
-        text-shadow: #fff 0px 0px 15px;
-        position: absolute;
-        left: -7px;
-        top: -7px;
-      }
-      .icon-edit,.icon-copy{
-        color: #555555;
-        font-weight: bold;
-        text-shadow: #ffffff -8px 8px 10px;
-        background-color: rgba(255,255,255,0.6);
-        position: absolute;
-        padding: 5px 5px;
+    .thumbnail {
+      .corner-icon {
+        display: none;
+        .icon-remove-sign{
+          color: #000000;
+          text-shadow: #fff 0px 0px 15px;
+          position: absolute;
+          left: -7px;
+          top: -7px;
+        }
+        .icon-edit, .icon-copy, .icon-save {
+          color: #555555;
+          font-weight: bold;
+          text-shadow: #ffffff -8px 8px 10px;
+          background-color: rgba(255,255,255,0.6);
+          position: absolute;
+          padding: 5px 5px;
+        }
+        .icon-copy {
+          right: 45px;
+        }
+        .icon-edit {
+          right: 25px;
+        }
+        .icon-save {
+          right: 5px;
+        }
       }
-      .icon-copy {
-        right: 25px;
+      .export-graph-list {
+        right: -1px;
       }
-      .icon-edit {
-        right: 5px;
+      &:hover {
+        cursor: move;
+        .corner-icon{
+          display: block;
+          text-decoration: none;
+          z-index: 9;
+        }
+        .caption{
+          margin-left: -10px;
+        }
       }
-    }
-    .thumbnail:hover {
-      cursor: move;
-      .corner-icon{
-        display: block;
+      & .hidden-description{
+        display: none;
+        color: #555555;
+        z-index: 7;
+        font-size: 12px;
+        font-weight: bold;
+        line-height: 18px;
+        text-align: center;
         text-decoration: none;
-        z-index: 9;
-      }
-      .caption{
-        margin-left: -10px;
+        position: absolute;
+        top: 40px;
+        padding: 8px 5px;
+        width: 89%;
+        height: 62%;
+        overflow: scroll;
+        white-space: pre-line;
+        background: rgba(255,255,255, 0.7);
       }
     }
-    .thumbnail .hidden-description{
-      display: none;
-      color: #555555;
-      z-index: 7;
-      font-size: 12px;
-      font-weight: bold;
-      line-height: 18px;
-      text-align: center;
-      text-decoration: none;
-      position: absolute;
-      top: 40px;
-      padding: 8px 5px;
-      width: 89%;
-      height: 62%;
-      overflow: scroll;
-      white-space: pre-line;
-      background: rgba(255,255,255, 0.7);
-    }
   }
   .thumbnail .chart-legend {
     .description-line {

http://git-wip-us.apache.org/repos/asf/ambari/blob/3737010b/ambari-web/app/templates/common/chart/linear_time.hbs
----------------------------------------------------------------------
diff --git a/ambari-web/app/templates/common/chart/linear_time.hbs b/ambari-web/app/templates/common/chart/linear_time.hbs
index b488dd3..35a3f3f 100644
--- a/ambari-web/app/templates/common/chart/linear_time.hbs
+++ b/ambari-web/app/templates/common/chart/linear_time.hbs
@@ -17,7 +17,18 @@
 }}
 
 <div {{bindAttr class="view.isReady:hide:show :screensaver :no-borders :chart-container"}}></div>
-<div {{bindAttr class="view.isReady::hidden :time-label"}}>{{view.parentView.currentTimeState.name}}</div>
+<div {{bindAttr class="view.isReady::hidden :time-label"}}>
+  {{view.parentView.currentTimeState.name}}
+  {{#if view.parentView.graph.hasData}}
+    <a class="corner-icon pull-right" href="#" {{action toggleFormatsList target="view"}}>
+      <i class="icon-save"></i>
+    </a>
+    <ul class="export-graph-list pull-right dropdown-menu">
+      <li><a {{action exportGraphData view.parentView.graph.exportToCSVArgument
target="view"}}>{{t common.csv}}</a></li>
+      <li><a {{action exportGraphData target="view"}}>{{t common.json}}</a></li>
+    </ul>
+  {{/if}}
+</div>
 {{#if view.isTimePagingEnable}}
   <div {{bindAttr class="view.leftArrowVisible:visibleArrow :arrow-left"}} {{action "switchTimeBack"
target="view.parentView"}}></div>
 {{/if}}

http://git-wip-us.apache.org/repos/asf/ambari/blob/3737010b/ambari-web/app/templates/common/widget/graph_widget.hbs
----------------------------------------------------------------------
diff --git a/ambari-web/app/templates/common/widget/graph_widget.hbs b/ambari-web/app/templates/common/widget/graph_widget.hbs
index dd5fd1d..6632db8 100644
--- a/ambari-web/app/templates/common/widget/graph_widget.hbs
+++ b/ambari-web/app/templates/common/widget/graph_widget.hbs
@@ -29,6 +29,13 @@
       <a class="corner-icon pull-right" href="#" {{action editWidget target="view"}}>
         <i class="icon-edit"></i>
       </a>
+      <a class="corner-icon pull-right" href="#" {{action toggleFormatsList target="view"}}>
+        <i class="icon-save"></i>
+      </a>
+      <ul class="export-graph-list pull-right dropdown-menu">
+        <li><a {{action exportGraphData view.exportToCSVArgument target="view"}}>{{t
common.csv}}</a></li>
+        <li><a {{action exportGraphData target="view"}}>{{t common.json}}</a></li>
+      </ul>
     {{/isAccessible}}
     <div class="content"> {{view view.graphView}}</div>
 

http://git-wip-us.apache.org/repos/asf/ambari/blob/3737010b/ambari-web/app/templates/main/charts/linear_time.hbs
----------------------------------------------------------------------
diff --git a/ambari-web/app/templates/main/charts/linear_time.hbs b/ambari-web/app/templates/main/charts/linear_time.hbs
index 24c13e9..f91f149 100644
--- a/ambari-web/app/templates/main/charts/linear_time.hbs
+++ b/ambari-web/app/templates/main/charts/linear_time.hbs
@@ -26,7 +26,19 @@
   <div id="{{unbound view.id}}-chart" class="chart"  {{action showGraphInPopup target="view"}}></div>
   <div id="{{unbound view.id}}-timeline" class="timeline" {{action showGraphInPopup target="view"}}></div>
   {{#unless view.noTitleUnderGraph}}
-    <div id="{{unbound view.id}}-title" class="chart-title">{{view.title}}</div>
+    <div id="{{unbound view.id}}-title" class="chart-title">
+      {{view.title}}
+    </div>
+    {{#if view.isReady}}
+      <a class="corner-icon span1" href="#" {{action toggleFormatsList target="view"}}>
+        <i class="icon-save"></i>
+      </a>
+      <div class="export-graph-list-top"></div>
+      <ul class="export-graph-list pull-right dropdown-menu">
+        <li><a {{action exportGraphData view.exportToCSVArgument target="view"}}>{{t
common.csv}}</a></li>
+        <li><a {{action exportGraphData target="view"}}>{{t common.json}}</a></li>
+      </ul>
+    {{/if}}
   {{/unless}}
 </div>
 

http://git-wip-us.apache.org/repos/asf/ambari/blob/3737010b/ambari-web/app/templates/main/dashboard/widgets/cluster_metrics.hbs
----------------------------------------------------------------------
diff --git a/ambari-web/app/templates/main/dashboard/widgets/cluster_metrics.hbs b/ambari-web/app/templates/main/dashboard/widgets/cluster_metrics.hbs
index 6156906..6feeb86 100644
--- a/ambari-web/app/templates/main/dashboard/widgets/cluster_metrics.hbs
+++ b/ambari-web/app/templates/main/dashboard/widgets/cluster_metrics.hbs
@@ -23,6 +23,17 @@
           <i class="icon-remove-sign icon-large"></i>
       </a>
       <div class="caption span10">{{view.title}}</div>
+      {{#if view.isDataLoaded}}
+        {{#if view.childViews.firstObject.hasData}}
+          <a class="corner-icon span1" href="#" {{action toggleFormatsList target="view"}}>
+            <i class="icon-save"></i>
+          </a>
+          <ul class="export-graph-list pull-right dropdown-menu">
+            <li><a {{action exportGraphData view.exportToCSVArgument target="view"}}>{{t
common.csv}}</a></li>
+            <li><a {{action exportGraphData target="view"}}>{{t common.json}}</a></li>
+          </ul>
+        {{/if}}
+      {{/if}}
 
       <div class="widget-content" >
         {{view view.content}}

http://git-wip-us.apache.org/repos/asf/ambari/blob/3737010b/ambari-web/app/utils/file_utils.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/utils/file_utils.js b/ambari-web/app/utils/file_utils.js
new file mode 100644
index 0000000..59a9416
--- /dev/null
+++ b/ambari-web/app/utils/file_utils.js
@@ -0,0 +1,79 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var stringUtils = require('utils/string_utils');
+
+module.exports = {
+
+  fileTypeMap: {
+    csv: 'text/csv',
+    json: 'application/json'
+  },
+
+  /**
+   * download text file
+   * @param data {String}
+   * @param fileType {String}
+   * @param fileName {String}
+   */
+  downloadTextFile: function (data, fileType, fileName) {
+    if ($.browser.msie && $.browser.version < 10) {
+      this.openInfoInNewTab(data);
+    } else if (typeof safari !== 'undefined') {
+      this.safariDownload(data, fileType, fileName);
+    } else {
+      try {
+        var blob = new Blob([data], {
+          type: (this.fileTypeMap[fileType] || 'text/' + fileType) + ';charset=utf-8;'
+        });
+        saveAs(blob, fileName);
+      } catch (e) {
+        this.openInfoInNewTab(data);
+      }
+    }
+  },
+
+  /**
+   * open content of text file in new window
+   * @param data {String}
+   */
+  openInfoInNewTab: function (data) {
+    var newWindow = window.open('');
+    var newDocument = newWindow.document;
+    newDocument.write(data);
+    newWindow.focus();
+  },
+
+  /**
+   * Hack to dowload text data in Safari
+   * @param data {String}
+   * @param fileType {String}
+   * @param fileName {String}
+   */
+  safariDownload: function (data, fileType, fileName) {
+    var file = 'data:attachment/' + fileType + ';charset=utf-8,' + encodeURI(data);
+    var linkEl = document.createElement("a");
+    linkEl.href = file;
+    linkEl.download = fileName;
+
+    document.body.appendChild(linkEl);
+    linkEl.click();
+    document.body.removeChild(linkEl);
+  }
+
+};

http://git-wip-us.apache.org/repos/asf/ambari/blob/3737010b/ambari-web/app/views/common/chart/linear_time.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/views/common/chart/linear_time.js b/ambari-web/app/views/common/chart/linear_time.js
index 97e3d7d..4cb496b 100644
--- a/ambari-web/app/views/common/chart/linear_time.js
+++ b/ambari-web/app/views/common/chart/linear_time.js
@@ -46,7 +46,7 @@ var dateUtils = require('utils/date');
  * @extends Ember.Object
  * @extends Ember.View
  */
-App.ChartLinearTimeView = Ember.View.extend({
+App.ChartLinearTimeView = Ember.View.extend(App.ExportMetricsMixin, {
   templateName: require('templates/main/charts/linear_time'),
 
   /**
@@ -163,12 +163,28 @@ App.ChartLinearTimeView = Ember.View.extend({
   didInsertElement: function () {
     this.loadData();
     this.registerGraph();
+    this.$().parent().on('mouseleave', function () {
+      $(this).find('.export-graph-list').hide();
+    });
     App.tooltip(this.$("[rel='ZoomInTooltip']"), {
       placement: 'left',
       template: '<div class="tooltip"><div class="tooltip-arrow"></div><div
class="tooltip-inner graph-tooltip"></div></div>'
     });
   },
 
+  setExportTooltip: function () {
+    if (this.get('isReady')) {
+      Em.run.next(this, function () {
+        this.$('.corner-icon').on('mouseover', function () {
+          $(this).closest("[rel='ZoomInTooltip']").trigger('mouseleave');
+        });
+        App.tooltip(this.$('.corner-icon > .icon-save'), {
+          title: Em.I18n.t('common.export')
+        });
+      });
+    }
+  }.observes('isReady'),
+
   willDestroyElement: function () {
     this.$("[rel='ZoomInTooltip']").tooltip('destroy');
     $(this.get('_containerSelector') + ' li.line').off();
@@ -391,7 +407,9 @@ App.ChartLinearTimeView = Ember.View.extend({
     }
     else {
       graph_container.children().each(function () {
-        $(this).children().remove();
+        if (!($(this).is('.export-graph-list, .corner-icon'))) {
+          $(this).children().remove();
+        }
       });
     }
     if (this.checkSeries(seriesData)) {
@@ -713,7 +731,7 @@ App.ChartLinearTimeView = Ember.View.extend({
     var self = this;
 
     App.ModalPopup.show({
-      bodyClass: Em.View.extend({
+      bodyClass: Em.View.extend(App.ExportMetricsMixin, {
 
         containerId: null,
         containerClass: null,
@@ -733,6 +751,14 @@ App.ChartLinearTimeView = Ember.View.extend({
         }.property('parentView.graph.isPopupReady'),
 
         didInsertElement: function () {
+          App.tooltip(this.$('.corner-icon > .icon-save'), {
+            title: Em.I18n.t('common.export')
+          });
+          this.$().closest('.modal').on('click', function (event) {
+            if (!($(event.target).is('.corner-icon, .icon-save, .export-graph-list, .export-graph-list
*'))) {
+              $(this).find('.export-graph-list').hide();
+            }
+          });
           $('#modal').addClass('modal-graph-line');
           var popupSuffix = this.get('parentView.graph.popupSuffix');
           var id = this.get('parentView.graph.id');
@@ -766,8 +792,17 @@ App.ChartLinearTimeView = Ember.View.extend({
 
         leftArrowVisible: function () {
           return (this.get('isReady') && (this.get('parentView.currentTimeIndex')
!= 7));
-        }.property('isReady', 'parentView.currentTimeIndex')
+        }.property('isReady', 'parentView.currentTimeIndex'),
 
+        exportGraphData: function (event) {
+          this._super();
+          var ajaxIndex = this.get('parentView.graph.ajaxIndex'),
+            isCSV = !!event.context,
+            targetView = ajaxIndex ? this.get('parentView.graph') : self.get('parentView');
+            targetView.exportGraphData({
+              context: event.context
+            });
+        }
       }),
       header: this.get('title'),
       /**
@@ -847,7 +882,21 @@ App.ChartLinearTimeView = Ember.View.extend({
   currentTimeIndex: 0,
   timeUnitSeconds: function () {
     return this.get('timeStates').objectAt(this.get('currentTimeIndex')).seconds;
-  }.property('currentTimeIndex')
+  }.property('currentTimeIndex'),
+
+  exportGraphData: function (event) {
+    this._super();
+    var ajaxIndex = this.get('ajaxIndex');
+    App.ajax.send({
+      name: ajaxIndex,
+      data: $.extend(this.getDataForAjaxRequest(), {
+        isCSV: !!event.context
+      }),
+      sender: this,
+      success: 'exportGraphDataSuccessCallback',
+      error: 'exportGraphDataErrorCallback'
+    });
+  }
 });
 
 /**

http://git-wip-us.apache.org/repos/asf/ambari/blob/3737010b/ambari-web/app/views/common/widget/graph_widget_view.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/views/common/widget/graph_widget_view.js b/ambari-web/app/views/common/widget/graph_widget_view.js
index b5abc96..3ad0419 100644
--- a/ambari-web/app/views/common/widget/graph_widget_view.js
+++ b/ambari-web/app/views/common/widget/graph_widget_view.js
@@ -18,7 +18,9 @@
 
 var App = require('app');
 
-App.GraphWidgetView = Em.View.extend(App.WidgetMixin, {
+var fileUtils = require('utils/file_utils');
+
+App.GraphWidgetView = Em.View.extend(App.WidgetMixin, App.ExportMetricsMixin, {
   templateName: require('templates/common/widget/graph_widget'),
 
   /**
@@ -286,6 +288,9 @@ App.GraphWidgetView = Em.View.extend(App.WidgetMixin, {
     },
 
     didInsertElement: function () {
+      this.$().closest('.graph-widget').on('mouseleave', function () {
+        $(this).find('.export-graph-list').hide();
+      });
       this.setYAxisFormatter();
       this.loadData();
       var self = this;
@@ -300,5 +305,20 @@ App.GraphWidgetView = Em.View.extend(App.WidgetMixin, {
         }
       });
     }.observes('parentView.data')
-  })
+  }),
+
+  toggleFormatsList: function () {
+    this.get('childViews.firstObject').$().closest('.graph-widget').find('.export-graph-list').toggle();
+  },
+
+  exportGraphData: function (event) {
+    this._super();
+    var data,
+      isCSV = !!event.context,
+      fileType = isCSV ? 'csv' : 'json',
+      fileName = 'data.' + fileType,
+      metrics = this.get('content.metrics'),
+      data = isCSV ? this.prepareCSV(metrics) : this.prepareJSON(metrics);
+    fileUtils.downloadTextFile(data, fileType, fileName);
+  }
 });
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ambari/blob/3737010b/ambari-web/app/views/main/dashboard/widgets/cluster_metrics_widget.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/views/main/dashboard/widgets/cluster_metrics_widget.js b/ambari-web/app/views/main/dashboard/widgets/cluster_metrics_widget.js
index d6b2db7..69bf666 100644
--- a/ambari-web/app/views/main/dashboard/widgets/cluster_metrics_widget.js
+++ b/ambari-web/app/views/main/dashboard/widgets/cluster_metrics_widget.js
@@ -18,8 +18,31 @@
 
 var App = require('app');
 
-App.ClusterMetricsDashboardWidgetView = App.DashboardWidgetView.extend({
+App.ClusterMetricsDashboardWidgetView = App.DashboardWidgetView.extend(App.ExportMetricsMixin,
{
 
-  templateName: require('templates/main/dashboard/widgets/cluster_metrics')
+  templateName: require('templates/main/dashboard/widgets/cluster_metrics'),
+
+  didInsertElement: function () {
+    this.$().on('mouseleave', function () {
+      $(this).find('.export-graph-list').hide();
+    });
+    App.tooltip(this.$('.corner-icon > .icon-save'), {
+      title: Em.I18n.t('common.export')
+    });
+  },
+
+  exportGraphData: function (event) {
+    this._super();
+    var ajaxIndex = this.get('childViews.firstObject.ajaxIndex');
+    App.ajax.send({
+      name: ajaxIndex,
+      data: $.extend(this.get('childViews.firstObject').getDataForAjaxRequest(), {
+        isCSV: !!event.context
+      }),
+      sender: this,
+      success: 'exportGraphDataSuccessCallback',
+      error: 'exportGraphDataErrorCallback'
+    });
+  }
 
 });

http://git-wip-us.apache.org/repos/asf/ambari/blob/3737010b/ambari-web/test/mixins/common/widgets/export_metrics_mixin_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/mixins/common/widgets/export_metrics_mixin_test.js b/ambari-web/test/mixins/common/widgets/export_metrics_mixin_test.js
new file mode 100644
index 0000000..79fbfdb
--- /dev/null
+++ b/ambari-web/test/mixins/common/widgets/export_metrics_mixin_test.js
@@ -0,0 +1,263 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var App = require('app');
+
+require('mixins/common/widgets/export_metrics_mixin');
+var fileUtils = require('utils/file_utils');
+
+describe('App.ExportMetricsMixin', function () {
+
+  var obj;
+
+  beforeEach(function () {
+    obj = Em.Object.create(App.ExportMetricsMixin);
+  });
+
+  describe('#exportGraphData', function () {
+
+    beforeEach(function () {
+      sinon.stub(obj, 'toggleFormatsList', Em.K);
+    });
+
+    afterEach(function () {
+      obj.toggleFormatsList.restore();
+    });
+
+    it('should toggle formats menu', function () {
+      obj.exportGraphData();
+      expect(obj.toggleFormatsList.calledOnce).to.be.true;
+    });
+
+  });
+
+  describe('#exportGraphDataSuccessCallback', function () {
+
+    var cases = [
+      {
+        response: null,
+        showAlertPopupCallCount: 1,
+        prepareCSVCallCount: 0,
+        prepareJSONCallCount: 0,
+        downloadTextFileCallCount: 0,
+        title: 'no response'
+      },
+      {
+        response: {
+          metrics: null
+        },
+        showAlertPopupCallCount: 1,
+        prepareCSVCallCount: 0,
+        prepareJSONCallCount: 0,
+        downloadTextFileCallCount: 0,
+        title: 'no metrics object in response'
+      },
+      {
+        response: {
+          metrics: {}
+        },
+        showAlertPopupCallCount: 1,
+        prepareCSVCallCount: 0,
+        prepareJSONCallCount: 0,
+        downloadTextFileCallCount: 0,
+        title: 'empty metrics object'
+      },
+      {
+        response: {
+          metrics: {
+            m0: [0, 1]
+          }
+        },
+        params: {
+          isCSV: true
+        },
+        showAlertPopupCallCount: 0,
+        prepareCSVCallCount: 1,
+        prepareJSONCallCount: 0,
+        downloadTextFileCallCount: 1,
+        fileType: 'csv',
+        fileName: 'data.csv',
+        title: 'export to CSV'
+      },
+      {
+        response: {
+          metrics: {
+            m0: [0, 1]
+          }
+        },
+        params: {
+          isCSV: false
+        },
+        showAlertPopupCallCount: 0,
+        prepareCSVCallCount: 0,
+        prepareJSONCallCount: 1,
+        downloadTextFileCallCount: 1,
+        fileType: 'json',
+        fileName: 'data.json',
+        title: 'export to JSON'
+      }
+    ];
+
+    beforeEach(function () {
+      sinon.stub(App, 'showAlertPopup', Em.K);
+      sinon.stub(fileUtils, 'downloadTextFile', Em.K);
+      sinon.stub(obj, 'prepareCSV', Em.K);
+      sinon.stub(obj, 'prepareJSON', Em.K);
+    });
+
+    afterEach(function () {
+      App.showAlertPopup.restore();
+      fileUtils.downloadTextFile.restore();
+      obj.prepareCSV.restore();
+      obj.prepareJSON.restore();
+    });
+
+    cases.forEach(function (item) {
+      it(item.title, function () {
+        obj.exportGraphDataSuccessCallback(item.response, null, item.params);
+        expect(obj.prepareCSV.callCount).to.equal(item.prepareCSVCallCount);
+        expect(obj.prepareJSON.callCount).to.equal(item.prepareJSONCallCount);
+        expect(fileUtils.downloadTextFile.callCount).to.equal(item.downloadTextFileCallCount);
+        if (item.downloadTextFileCallCount) {
+          expect(fileUtils.downloadTextFile.firstCall.args[1]).to.equal(item.fileType);
+          expect(fileUtils.downloadTextFile.firstCall.args[2]).to.equal(item.fileName);
+        }
+      });
+    });
+
+  });
+
+  describe('#exportGraphDataErrorCallback', function () {
+
+    beforeEach(function () {
+      sinon.stub(App.ajax, 'defaultErrorHandler', Em.K);
+    });
+
+    afterEach(function () {
+      App.ajax.defaultErrorHandler.restore();
+    });
+
+    it('should display error popup', function () {
+      obj.exportGraphDataErrorCallback({
+          status: 404
+        }, null, '', {
+          url: 'url',
+          method: 'GET'
+        });
+      expect(App.ajax.defaultErrorHandler.calledOnce).to.be.true;
+      expect(App.ajax.defaultErrorHandler.calledWith({
+          status: 404
+        }, 'url', 'GET', 404)).to.be.true;
+    });
+
+  });
+
+  describe('#setMetricsArrays', function () {
+
+    var metrics = [],
+      titles = [],
+      data = {
+        key0: {
+          key1: {
+            key2: [[0, 1], [2, 3]],
+            key3: [[4, 5], [6, 7]]
+          }
+        }
+      };
+
+    it('should construct arrays with metrics info', function () {
+      obj.setMetricsArrays(data, metrics, titles);
+      expect(metrics).to.eql([[[0, 1], [2, 3]], [[4, 5], [6, 7]]]);
+      expect(titles).to.eql(['key2', 'key3']);
+    })
+
+  });
+
+  describe('#prepareCSV', function () {
+
+    var cases = [
+      {
+        data: {
+          metrics: {
+            key0: [[0, 1], [2, 3]],
+            key1: [[4, 1], [5, 3]]
+          }
+        },
+        result: 'Timestamp,key0,key1\n1,0,4\n3,2,5\n',
+        title: 'old style widget metrics'
+      },
+      {
+        data: [
+          {
+            data: [[6, 7], [8, 9]]
+          },
+          {
+            data: [[10, 7], [11, 9]]
+          }
+        ],
+        result: 'Timestamp,,\n7,6,10\n9,8,11\n',
+        title: 'enhanced widget metrics'
+      }
+    ];
+
+    cases.forEach(function (item) {
+      it(item.title, function () {
+        expect(obj.prepareCSV(item.data)).to.equal(item.result);
+      });
+    });
+
+  });
+
+  describe('#prepareJSON', function () {
+
+    var cases = [
+      {
+        data: {
+          metrics: {
+            key0: [[0, 1], [2, 3]],
+            key1: [[4, 1], [5, 3]]
+          }
+        },
+        result: "{\"key0\":[[0,1],[2,3]],\"key1\":[[4,1],[5,3]]}",
+        title: 'old style widget metrics'
+      },
+      {
+        data: [
+          {
+            name: 'n0',
+            data: [[6, 7], [8, 9]]
+          },
+          {
+            name: 'n1',
+            data: [[10, 7], [11, 9]]
+          }
+        ],
+        result: "[{\"name\":\"n0\",\"data\":[[6,7],[8,9]]},{\"name\":\"n1\",\"data\":[[10,7],[11,9]]}]",
+        title: 'enhanced widget metrics'
+      }
+    ];
+
+    cases.forEach(function (item) {
+      it(item.title, function () {
+        expect(obj.prepareJSON(item.data).replace(/\s/g, '')).to.equal(item.result);
+      });
+    });
+
+  });
+
+});


Mime
View raw message