You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by al...@apache.org on 2015/10/05 19:15:05 UTC
ambari git commit: AMBARI-13313. Ability to export graphs as CSV
Repository: ambari
Updated Branches:
refs/heads/trunk ce8238b60 -> ef80b5ef3
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/ef80b5ef
Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/ef80b5ef
Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/ef80b5ef
Branch: refs/heads/trunk
Commit: ef80b5ef38f8761b69ea2c01cf225d36e39c58a1
Parents: ce8238b
Author: Alex Antonenko <hi...@gmail.com>
Authored: Mon Oct 5 20:12:14 2015 +0300
Committer: Alex Antonenko <hi...@gmail.com>
Committed: Mon Oct 5 20:15:00 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/ef80b5ef/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 8ace78c..4a5af5c 100644
--- a/ambari-web/app/assets/test/tests.js
+++ b/ambari-web/app/assets/test/tests.js
@@ -140,6 +140,7 @@ var files = [
'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/ef80b5ef/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/ef80b5ef/ambari-web/app/messages.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/messages.js b/ambari-web/app/messages.js
index a3bb574..0d60a91 100644
--- a/ambari-web/app/messages.js
+++ b/ambari-web/app/messages.js
@@ -273,6 +273,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...',
'common.running': 'Running',
'common.stopped': 'Stopped',
http://git-wip-us.apache.org/repos/asf/ambari/blob/ef80b5ef/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/ef80b5ef/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/ef80b5ef/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 078ae98..a0d507b 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/ef80b5ef/ambari-web/app/styles/application.less
----------------------------------------------------------------------
diff --git a/ambari-web/app/styles/application.less b/ambari-web/app/styles/application.less
index 2b1f2f5..46e3d4a 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/ef80b5ef/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/ef80b5ef/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/ef80b5ef/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/ef80b5ef/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/ef80b5ef/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/ef80b5ef/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/ef80b5ef/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/ef80b5ef/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/ef80b5ef/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/ef80b5ef/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/ef80b5ef/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);
+ });
+ });
+
+ });
+
+});