Преглед изворни кода

Merge branch 'master' into panelbase

Torkel Ödegaard пре 10 година
родитељ
комит
7b4fe824ec

+ 5 - 2
CHANGELOG.md

@@ -1,6 +1,6 @@
 # 3.0.0 (unrelased master branch)
 # 3.0.0 (unrelased master branch)
 
 
-### New Features ###
+### New Features
 * **Playlists**: Playlists can now be persisted and started from urls, closes [#3655](https://github.com/grafana/grafana/pull/3655)
 * **Playlists**: Playlists can now be persisted and started from urls, closes [#3655](https://github.com/grafana/grafana/pull/3655)
 * **Metadata**: Settings panel now shows dashboard metadata, closes [#3304](https://github.com/grafana/grafana/issues/3304)
 * **Metadata**: Settings panel now shows dashboard metadata, closes [#3304](https://github.com/grafana/grafana/issues/3304)
 * **InfluxDB**: Support for policy selection in query editor, closes [#2018](https://github.com/grafana/grafana/issues/2018)
 * **InfluxDB**: Support for policy selection in query editor, closes [#2018](https://github.com/grafana/grafana/issues/2018)
@@ -10,11 +10,14 @@
 **InfluxDB 0.8.x** The data source for the old version of influxdb (0.8.x) is no longer included in default builds. Can easily be installed via improved plugin system, closes #3523
 **InfluxDB 0.8.x** The data source for the old version of influxdb (0.8.x) is no longer included in default builds. Can easily be installed via improved plugin system, closes #3523
 **KairosDB** The data source is no longer included in default builds. Can easily be installed via improved plugin system, closes #3524
 **KairosDB** The data source is no longer included in default builds. Can easily be installed via improved plugin system, closes #3524
 
 
-### Enhancements ###
+### Enhancements
 * **Sessions**: Support for memcached as session storage, closes [#3458](https://github.com/grafana/grafana/pull/3458)
 * **Sessions**: Support for memcached as session storage, closes [#3458](https://github.com/grafana/grafana/pull/3458)
 * **mysql**: Grafana now supports ssl for mysql, closes [#3584](https://github.com/grafana/grafana/pull/3584)
 * **mysql**: Grafana now supports ssl for mysql, closes [#3584](https://github.com/grafana/grafana/pull/3584)
 * **snapshot**: Annotations are now included in snapshots, closes [#3635](https://github.com/grafana/grafana/pull/3635)
 * **snapshot**: Annotations are now included in snapshots, closes [#3635](https://github.com/grafana/grafana/pull/3635)
 
 
+### Bug fixes
+* **Playlist**: Fix for memory leak when running a playlist, closes [#3794](https://github.com/grafana/grafana/pull/3794)
+
 # 2.6.1 (unrelased, 2.6.x branch)
 # 2.6.1 (unrelased, 2.6.x branch)
 
 
 ### New Features
 ### New Features

+ 2 - 0
docs/sources/datasources/prometheus.md

@@ -51,6 +51,8 @@ Name | Description
 
 
 For details of `metric names` & `label names`, and `label values`, please refer to the [Prometheus documentation](http://prometheus.io/docs/concepts/data_model/#metric-names-and-labels).
 For details of `metric names` & `label names`, and `label values`, please refer to the [Prometheus documentation](http://prometheus.io/docs/concepts/data_model/#metric-names-and-labels).
 
 
+> Note: The part of queries is incompatible with the version before 2.6, if you specify like `foo.*`, please change like `metrics(foo.*)`.
+
 You can create a template variable in Grafana and have that variable filled with values from any Prometheus metric exploration query.
 You can create a template variable in Grafana and have that variable filled with values from any Prometheus metric exploration query.
 You can then use this variable in your Prometheus metric queries.
 You can then use this variable in your Prometheus metric queries.
 
 

+ 51 - 12
pkg/api/api_plugin.go

@@ -1,13 +1,17 @@
 package api
 package api
 
 
 import (
 import (
+	"bytes"
 	"encoding/json"
 	"encoding/json"
+	"fmt"
 	"net/http"
 	"net/http"
 	"net/http/httputil"
 	"net/http/httputil"
 	"net/url"
 	"net/url"
+	"text/template"
 
 
 	"gopkg.in/macaron.v1"
 	"gopkg.in/macaron.v1"
 
 
+	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/middleware"
 	"github.com/grafana/grafana/pkg/middleware"
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
@@ -34,32 +38,28 @@ func InitApiPluginRoutes(r *macaron.Macaron) {
 					handlers = append(handlers, middleware.RoleAuth(m.ROLE_EDITOR, m.ROLE_ADMIN))
 					handlers = append(handlers, middleware.RoleAuth(m.ROLE_EDITOR, m.ROLE_ADMIN))
 				}
 				}
 			}
 			}
-			handlers = append(handlers, ApiPlugin(route.Url))
+			handlers = append(handlers, ApiPlugin(route, plugin.IncludedInAppId))
 			r.Route(url, route.Method, handlers...)
 			r.Route(url, route.Method, handlers...)
 			log.Info("Plugin: Adding route %s", url)
 			log.Info("Plugin: Adding route %s", url)
 		}
 		}
 	}
 	}
 }
 }
 
 
-func ApiPlugin(routeUrl string) macaron.Handler {
+func ApiPlugin(route *plugins.ApiPluginRoute, includedInAppId string) macaron.Handler {
 	return func(c *middleware.Context) {
 	return func(c *middleware.Context) {
 		path := c.Params("*")
 		path := c.Params("*")
 
 
-		//Create a HTTP header with the context in it.
-		ctx, err := json.Marshal(c.SignedInUser)
-		if err != nil {
-			c.JsonApiErr(500, "failed to marshal context to json.", err)
-			return
-		}
-		targetUrl, _ := url.Parse(routeUrl)
-		proxy := NewApiPluginProxy(string(ctx), path, targetUrl)
+		proxy := NewApiPluginProxy(c, path, route, includedInAppId)
 		proxy.Transport = dataProxyTransport
 		proxy.Transport = dataProxyTransport
 		proxy.ServeHTTP(c.Resp, c.Req.Request)
 		proxy.ServeHTTP(c.Resp, c.Req.Request)
 	}
 	}
 }
 }
 
 
-func NewApiPluginProxy(ctx string, proxyPath string, targetUrl *url.URL) *httputil.ReverseProxy {
+func NewApiPluginProxy(ctx *middleware.Context, proxyPath string, route *plugins.ApiPluginRoute, includedInAppId string) *httputil.ReverseProxy {
+	targetUrl, _ := url.Parse(route.Url)
+
 	director := func(req *http.Request) {
 	director := func(req *http.Request) {
+
 		req.URL.Scheme = targetUrl.Scheme
 		req.URL.Scheme = targetUrl.Scheme
 		req.URL.Host = targetUrl.Host
 		req.URL.Host = targetUrl.Host
 		req.Host = targetUrl.Host
 		req.Host = targetUrl.Host
@@ -69,7 +69,46 @@ func NewApiPluginProxy(ctx string, proxyPath string, targetUrl *url.URL) *httput
 		// clear cookie headers
 		// clear cookie headers
 		req.Header.Del("Cookie")
 		req.Header.Del("Cookie")
 		req.Header.Del("Set-Cookie")
 		req.Header.Del("Set-Cookie")
-		req.Header.Add("Grafana-Context", ctx)
+
+		//Create a HTTP header with the context in it.
+		ctxJson, err := json.Marshal(ctx.SignedInUser)
+		if err != nil {
+			ctx.JsonApiErr(500, "failed to marshal context to json.", err)
+			return
+		}
+
+		req.Header.Add("Grafana-Context", string(ctxJson))
+		// add custom headers defined in the plugin config.
+		for _, header := range route.Headers {
+			var contentBuf bytes.Buffer
+			t, err := template.New("content").Parse(header.Content)
+			if err != nil {
+				ctx.JsonApiErr(500, fmt.Sprintf("could not parse header content template for header %s.", header.Name), err)
+				return
+			}
+
+			jsonData := make(map[string]interface{})
+
+			if includedInAppId != "" {
+				//lookup appSettings
+				query := m.GetAppSettingByAppIdQuery{OrgId: ctx.OrgId, AppId: includedInAppId}
+
+				if err := bus.Dispatch(&query); err != nil {
+					ctx.JsonApiErr(500, "failed to get AppSettings of includedAppId.", err)
+					return
+				}
+
+				jsonData = query.Result.JsonData
+			}
+
+			err = t.Execute(&contentBuf, jsonData)
+			if err != nil {
+				ctx.JsonApiErr(500, fmt.Sprintf("failed to execute header content template for header %s.", header.Name), err)
+				return
+			}
+			log.Debug("Adding header to proxy request. %s: %s", header.Name, contentBuf.String())
+			req.Header.Add(header.Name, contentBuf.String())
+		}
 	}
 	}
 
 
 	return &httputil.ReverseProxy{Director: director}
 	return &httputil.ReverseProxy{Director: director}

+ 1 - 0
pkg/api/dtos/apps.go

@@ -31,6 +31,7 @@ func NewAppSettingsDto(def *plugins.AppPlugin, data *models.AppSettings) *AppSet
 		dto.Enabled = data.Enabled
 		dto.Enabled = data.Enabled
 		dto.Pinned = data.Pinned
 		dto.Pinned = data.Pinned
 		dto.Info = &def.Info
 		dto.Info = &def.Info
+		dto.JsonData = data.JsonData
 	}
 	}
 
 
 	return dto
 	return dto

+ 14 - 1
pkg/models/app_settings.go

@@ -1,6 +1,13 @@
 package models
 package models
 
 
-import "time"
+import (
+	"errors"
+	"time"
+)
+
+var (
+	ErrAppSettingNotFound = errors.New("AppSetting not found")
+)
 
 
 type AppSettings struct {
 type AppSettings struct {
 	Id       int64
 	Id       int64
@@ -33,3 +40,9 @@ type GetAppSettingsQuery struct {
 	OrgId  int64
 	OrgId  int64
 	Result []*AppSettings
 	Result []*AppSettings
 }
 }
+
+type GetAppSettingByAppIdQuery struct {
+	AppId  string
+	OrgId  int64
+	Result *AppSettings
+}

+ 38 - 0
pkg/plugins/api_plugin.go

@@ -0,0 +1,38 @@
+package plugins
+
+import (
+	"encoding/json"
+
+	"github.com/grafana/grafana/pkg/models"
+)
+
+type ApiPluginRoute struct {
+	Path            string            `json:"path"`
+	Method          string            `json:"method"`
+	ReqSignedIn     bool              `json:"reqSignedIn"`
+	ReqGrafanaAdmin bool              `json:"reqGrafanaAdmin"`
+	ReqRole         models.RoleType   `json:"reqRole"`
+	Url             string            `json:"url"`
+	Headers         []ApiPluginHeader `json:"headers"`
+}
+
+type ApiPlugin struct {
+	PluginBase
+	Routes []*ApiPluginRoute `json:"routes"`
+}
+
+type ApiPluginHeader struct {
+	Name    string `json:"name"`
+	Content string `json:"content"`
+}
+
+func (app *ApiPlugin) Load(decoder *json.Decoder, pluginDir string) error {
+	if err := decoder.Decode(&app); err != nil {
+		return err
+	}
+
+	app.PluginDir = pluginDir
+
+	ApiPlugins[app.Id] = app
+	return nil
+}

+ 12 - 0
pkg/plugins/app_plugin.go

@@ -59,6 +59,18 @@ func (app *AppPlugin) Load(decoder *json.Decoder, pluginDir string) error {
 		}
 		}
 	}
 	}
 
 
+	// check if we have child apiPlugins
+	for _, plugin := range ApiPlugins {
+		if strings.HasPrefix(plugin.PluginDir, app.PluginDir) {
+			plugin.IncludedInAppId = app.Id
+			app.Includes = append(app.Includes, AppIncludeInfo{
+				Name: plugin.Name,
+				Id:   plugin.Id,
+				Type: plugin.Type,
+			})
+		}
+	}
+
 	Apps[app.Id] = app
 	Apps[app.Id] = app
 	return nil
 	return nil
 }
 }

+ 0 - 16
pkg/plugins/models.go

@@ -2,8 +2,6 @@ package plugins
 
 
 import (
 import (
 	"encoding/json"
 	"encoding/json"
-
-	"github.com/grafana/grafana/pkg/models"
 )
 )
 
 
 type PluginLoader interface {
 type PluginLoader interface {
@@ -44,20 +42,6 @@ type PluginStaticRoute struct {
 	PluginId  string
 	PluginId  string
 }
 }
 
 
-type ApiPluginRoute struct {
-	Path            string          `json:"path"`
-	Method          string          `json:"method"`
-	ReqSignedIn     bool            `json:"reqSignedIn"`
-	ReqGrafanaAdmin bool            `json:"reqGrafanaAdmin"`
-	ReqRole         models.RoleType `json:"reqRole"`
-	Url             string          `json:"url"`
-}
-
-type ApiPlugin struct {
-	PluginBase
-	Routes []*ApiPluginRoute `json:"routes"`
-}
-
 type EnabledPlugins struct {
 type EnabledPlugins struct {
 	Panels      []*PanelPlugin
 	Panels      []*PanelPlugin
 	DataSources map[string]*DataSourcePlugin
 	DataSources map[string]*DataSourcePlugin

+ 13 - 0
pkg/services/sqlstore/app_settings.go

@@ -9,6 +9,7 @@ import (
 
 
 func init() {
 func init() {
 	bus.AddHandler("sql", GetAppSettings)
 	bus.AddHandler("sql", GetAppSettings)
+	bus.AddHandler("sql", GetAppSettingByAppId)
 	bus.AddHandler("sql", UpdateAppSettings)
 	bus.AddHandler("sql", UpdateAppSettings)
 }
 }
 
 
@@ -19,6 +20,18 @@ func GetAppSettings(query *m.GetAppSettingsQuery) error {
 	return sess.Find(&query.Result)
 	return sess.Find(&query.Result)
 }
 }
 
 
+func GetAppSettingByAppId(query *m.GetAppSettingByAppIdQuery) error {
+	appSetting := m.AppSettings{OrgId: query.OrgId, AppId: query.AppId}
+	has, err := x.Get(&appSetting)
+	if err != nil {
+		return err
+	} else if has == false {
+		return m.ErrAppSettingNotFound
+	}
+	query.Result = &appSetting
+	return nil
+}
+
 func UpdateAppSettings(cmd *m.UpdateAppSettingsCmd) error {
 func UpdateAppSettings(cmd *m.UpdateAppSettingsCmd) error {
 	return inTransaction2(func(sess *session) error {
 	return inTransaction2(func(sess *session) error {
 		var app m.AppSettings
 		var app m.AppSettings

+ 37 - 0
public/app/core/utils/file_export.ts

@@ -0,0 +1,37 @@
+///<reference path="../../headers/common.d.ts" />
+
+import _ from 'lodash';
+
+declare var window: any;
+
+export function exportSeriesListToCsv(seriesList) {
+    var text = 'Series;Time;Value\n';
+    _.each(seriesList, function(series) {
+        _.each(series.datapoints, function(dp) {
+            text += series.alias + ';' + new Date(dp[1]).toISOString() + ';' + dp[0] + '\n';
+        });
+    });
+    saveSaveBlob(text, 'grafana_data_export.csv');
+};
+
+export function exportTableDataToCsv(table) {
+    var text = '';
+    // add header
+    _.each(table.columns, function(column) {
+        text += column.text + ';';
+    });
+    text += '\n';
+    // process data
+    _.each(table.rows, function(row) {
+        _.each(row, function(value) {
+            text += value + ';';
+        });
+        text += '\n';
+    });
+    saveSaveBlob(text, 'grafana_data_export.csv');
+};
+
+export function saveSaveBlob(payload, fname) {
+    var blob = new Blob([payload], { type: "text/csv;charset=utf-8" });
+    window.saveAs(blob, fname);
+};

+ 0 - 11
public/app/core/utils/kbn.js

@@ -179,17 +179,6 @@ function($, _) {
       .replace(/ +/g,'-');
       .replace(/ +/g,'-');
   };
   };
 
 
-  kbn.exportSeriesListToCsv = function(seriesList) {
-    var text = 'Series;Time;Value\n';
-    _.each(seriesList, function(series) {
-      _.each(series.datapoints, function(dp) {
-        text += series.alias + ';' + new Date(dp[1]).toISOString() + ';' + dp[0] + '\n';
-      });
-    });
-    var blob = new Blob([text], { type: "text/csv;charset=utf-8" });
-    window.saveAs(blob, 'grafana_data_export.csv');
-  };
-
   kbn.stringToJsRegex = function(str) {
   kbn.stringToJsRegex = function(str) {
     if (str[0] !== '/') {
     if (str[0] !== '/') {
       return new RegExp('^' + str + '$');
       return new RegExp('^' + str + '$');

+ 2 - 0
public/app/features/apps/partials/edit.html

@@ -98,6 +98,8 @@
 		<div class="simple-box-body">
 		<div class="simple-box-body">
 			<div ng-if="ctrl.appModel.appId">
 			<div ng-if="ctrl.appModel.appId">
 				<app-config-view app-model="ctrl.appModel"></app-config-view>
 				<app-config-view app-model="ctrl.appModel"></app-config-view>
+				<div class="clearfix"></div>
+				<button type="submit" class="btn btn-success" ng-click="ctrl.update()">Save</button>
 			</div>
 			</div>
 		</div>
 		</div>
 	</section>
 	</section>

+ 1 - 0
public/app/features/dashboard/submenu/submenu.ts

@@ -7,6 +7,7 @@ export class SubmenuCtrl {
   variables: any;
   variables: any;
   dashboard: any;
   dashboard: any;
 
 
+  /** @ngInject */
   constructor(private $rootScope, private templateValuesSrv, private dynamicDashboardSrv) {
   constructor(private $rootScope, private templateValuesSrv, private dynamicDashboardSrv) {
     this.annotations = this.dashboard.templating.list;
     this.annotations = this.dashboard.templating.list;
     this.variables = this.dashboard.templating.list;
     this.variables = this.dashboard.templating.list;

+ 1 - 1
public/app/features/dashlinks/module.js

@@ -159,7 +159,7 @@ function (angular, _) {
     };
     };
 
 
     updateDashLinks();
     updateDashLinks();
-    $rootScope.onAppEvent('dash-links-updated', updateDashLinks, $rootScope);
+    $rootScope.onAppEvent('dash-links-updated', updateDashLinks, $scope);
   });
   });
 
 
   module.controller('DashLinkEditorCtrl', function($scope, $rootScope) {
   module.controller('DashLinkEditorCtrl', function($scope, $rootScope) {

+ 0 - 5
public/app/features/playlist/partials/playlist-remove.html

@@ -1,5 +0,0 @@
-<p class="text-center">Are you sure want to delete "{{playlist.title}}" playlist?</p>
-<p class="text-center">
-  <button type="button" class="btn btn-danger" ng-click="removePlaylist()">Yes</button>
-  <button type="button" class="btn btn-default" ng-click="dismiss()">No</button>
-</p>

+ 2 - 2
public/app/features/playlist/playlist_edit_ctrl.js

@@ -132,11 +132,11 @@ function (angular, config, _) {
     };
     };
 
 
     $scope.movePlaylistItemUp = function(playlistItem) {
     $scope.movePlaylistItemUp = function(playlistItem) {
-      $scope.moveDashboard(playlistItem, -1);
+      $scope.movePlaylistItem(playlistItem, -1);
     };
     };
 
 
     $scope.movePlaylistItemDown = function(playlistItem) {
     $scope.movePlaylistItemDown = function(playlistItem) {
-      $scope.moveDashboard(playlistItem, 1);
+      $scope.movePlaylistItem(playlistItem, 1);
     };
     };
 
 
     $scope.init();
     $scope.init();

+ 2 - 2
public/app/features/playlist/playlist_srv.ts

@@ -1,6 +1,7 @@
 ///<reference path="../../headers/common.d.ts" />
 ///<reference path="../../headers/common.d.ts" />
 
 
 import angular from 'angular';
 import angular from 'angular';
+import config from 'app/core/config';
 import coreModule from '../../core/core_module';
 import coreModule from '../../core/core_module';
 import kbn from 'app/core/utils/kbn';
 import kbn from 'app/core/utils/kbn';
 
 
@@ -20,10 +21,9 @@ class PlaylistSrv {
     var playedAllDashboards = this.index > this.dashboards.length - 1;
     var playedAllDashboards = this.index > this.dashboards.length - 1;
 
 
     if (playedAllDashboards) {
     if (playedAllDashboards) {
-      this.start(this.playlistId);
+      window.location.href = `${config.appSubUrl}/playlists/play/${this.playlistId}`;
     } else {
     } else {
       var dash = this.dashboards[this.index];
       var dash = this.dashboards[this.index];
-
       this.$location.url('dashboard/' + dash.uri);
       this.$location.url('dashboard/' + dash.uri);
 
 
       this.index++;
       this.index++;

+ 1 - 0
public/app/grafana.ts

@@ -1,6 +1,7 @@
 ///<reference path="headers/common.d.ts" />
 ///<reference path="headers/common.d.ts" />
 
 
 import 'bootstrap';
 import 'bootstrap';
+import 'vendor/filesaver';
 import 'lodash-src';
 import 'lodash-src';
 import 'angular-strap';
 import 'angular-strap';
 import 'angular-route';
 import 'angular-route';

+ 0 - 56
public/app/plugins/datasource/elasticsearch/directives.js

@@ -1,56 +0,0 @@
-define([
-  'angular',
-  './bucket_agg',
-  './metric_agg',
-],
-function (angular) {
-  'use strict';
-
-  var module = angular.module('grafana.directives');
-
-  module.directive('metricQueryEditorElasticsearch', function() {
-    return {controller: 'ElasticQueryCtrl', templateUrl: 'app/plugins/datasource/elasticsearch/partials/query.editor.html'};
-  });
-
-  module.directive('metricQueryOptionsElasticsearch', function() {
-    return {templateUrl: 'app/plugins/datasource/elasticsearch/partials/query.options.html'};
-  });
-
-  module.directive('annotationsQueryEditorElasticsearch', function() {
-    return {templateUrl: 'app/plugins/datasource/elasticsearch/partials/annotations.editor.html'};
-  });
-
-  module.directive('elastic', function() {
-    return {templateUrl: 'app/plugins/datasource/elasticsearch/partials/config.html'};
-  });
-
-  module.directive('elasticMetricAgg', function() {
-    return {
-      templateUrl: 'app/plugins/datasource/elasticsearch/partials/metric_agg.html',
-      controller: 'ElasticMetricAggCtrl',
-      restrict: 'E',
-      scope: {
-        target: "=",
-        index: "=",
-        onChange: "&",
-        getFields: "&",
-        esVersion: '='
-      }
-    };
-  });
-
-  module.directive('elasticBucketAgg', function() {
-    return {
-      templateUrl: 'app/plugins/datasource/elasticsearch/partials/bucket_agg.html',
-      controller: 'ElasticBucketAggCtrl',
-      restrict: 'E',
-      scope: {
-        target: "=",
-        index: "=",
-        onChange: "&",
-        getFields: "&",
-      }
-    };
-  });
-
-});

+ 1 - 0
public/app/plugins/datasource/mixed/datasource.ts

@@ -5,6 +5,7 @@ import _ from 'lodash';
 
 
 class MixedDatasource {
 class MixedDatasource {
 
 
+  /** @ngInject */
   constructor(private $q, private datasourceSrv) {
   constructor(private $q, private datasourceSrv) {
   }
   }
 
 

+ 3 - 2
public/app/plugins/panel/graph/module.js

@@ -3,13 +3,14 @@ define([
   'lodash',
   'lodash',
   'moment',
   'moment',
   'app/core/utils/kbn',
   'app/core/utils/kbn',
+  'app/core/utils/file_export',
   'app/core/time_series',
   'app/core/time_series',
   'app/features/panel/panel_meta',
   'app/features/panel/panel_meta',
   './seriesOverridesCtrl',
   './seriesOverridesCtrl',
   './graph',
   './graph',
   './legend',
   './legend',
 ],
 ],
-function (angular, _, moment, kbn, TimeSeries, PanelMeta) {
+function (angular, _, moment, kbn, fileExport, TimeSeries, PanelMeta) {
   'use strict';
   'use strict';
 
 
   /** @ngInject */
   /** @ngInject */
@@ -282,7 +283,7 @@ function (angular, _, moment, kbn, TimeSeries, PanelMeta) {
     };
     };
 
 
     $scope.exportCsv = function() {
     $scope.exportCsv = function() {
-      kbn.exportSeriesListToCsv($scope.seriesList);
+      fileExport.exportSeriesListToCsv($scope.seriesList);
     };
     };
 
 
     panelSrv.init($scope);
     panelSrv.init($scope);

+ 6 - 0
public/app/plugins/panel/table/controller.ts

@@ -3,6 +3,7 @@
 import angular from 'angular';
 import angular from 'angular';
 import _ from 'lodash';
 import _ from 'lodash';
 import moment from 'moment';
 import moment from 'moment';
+import * as FileExport from 'app/core/utils/file_export';
 import PanelMeta from 'app/features/panel/panel_meta2';
 import PanelMeta from 'app/features/panel/panel_meta2';
 import {transformDataToTable} from './transformers';
 import {transformDataToTable} from './transformers';
 
 
@@ -22,6 +23,7 @@ export class TablePanelCtrl {
 
 
     $scope.panelMeta.addEditorTab('Options', 'app/plugins/panel/table/options.html');
     $scope.panelMeta.addEditorTab('Options', 'app/plugins/panel/table/options.html');
     $scope.panelMeta.addEditorTab('Time range', 'app/features/panel/partials/panelTime.html');
     $scope.panelMeta.addEditorTab('Time range', 'app/features/panel/partials/panelTime.html');
+    $scope.panelMeta.addExtendedMenuItem('Export CSV', '', 'exportCsv()');
 
 
     var panelDefaults = {
     var panelDefaults = {
       targets: [{}],
       targets: [{}],
@@ -124,6 +126,10 @@ export class TablePanelCtrl {
       panelHelper.broadcastRender($scope, $scope.table, $scope.dataRaw);
       panelHelper.broadcastRender($scope, $scope.table, $scope.dataRaw);
     };
     };
 
 
+    $scope.exportCsv = function() {
+      FileExport.exportTableDataToCsv($scope.table);
+    };
+
     $scope.init();
     $scope.init();
   }
   }
 }
 }

+ 1 - 0
public/less/sidemenu.less

@@ -13,6 +13,7 @@
   min-height: 100%;
   min-height: 100%;
   z-index: 101;
   z-index: 101;
   transform: translate3d(-100%, 0, 0);
   transform: translate3d(-100%, 0, 0);
+  visibility: hidden;
 
 
   a:focus {
   a:focus {
     text-decoration: none;
     text-decoration: none;

+ 2 - 0
public/views/index.html

@@ -56,8 +56,10 @@
 	</script>
 	</script>
 
 
 	<!-- build:js [[.AppSubUrl]]/public/app/boot.js -->
 	<!-- build:js [[.AppSubUrl]]/public/app/boot.js -->
+	<script src="[[.AppSubUrl]]/public/vendor/npm/es5-shim/es5-shim.js"></script>
 	<script src="[[.AppSubUrl]]/public/vendor/npm/es6-shim/es6-shim.js"></script>
 	<script src="[[.AppSubUrl]]/public/vendor/npm/es6-shim/es6-shim.js"></script>
 	<script src="[[.AppSubUrl]]/public/vendor/npm/es6-promise/dist/es6-promise.js"></script>
 	<script src="[[.AppSubUrl]]/public/vendor/npm/es6-promise/dist/es6-promise.js"></script>
+	<script src="[[.AppSubUrl]]/public/vendor/npm/systemjs/dist/system-polyfills.js"></script>
 	<script src="[[.AppSubUrl]]/public/vendor/npm/systemjs/dist/system.src.js"></script>
 	<script src="[[.AppSubUrl]]/public/vendor/npm/systemjs/dist/system.src.js"></script>
 	<script src="[[.AppSubUrl]]/public/app/system.conf.js"></script>
 	<script src="[[.AppSubUrl]]/public/app/system.conf.js"></script>
 	<script src="[[.AppSubUrl]]/public/app/boot.js"></script>
 	<script src="[[.AppSubUrl]]/public/app/boot.js"></script>

+ 3 - 1
symlink_git_hooks.sh

@@ -1,3 +1,5 @@
 #/bin/bash
 #/bin/bash
 
 
-ln -s .hooks/* .git/hooks/
+#ln -s -f .hooks/* .git/hooks/
+cd .git/hooks/
+cp --symbolic-link -f ../../.hooks/* .

+ 1 - 1
tasks/build_task.js

@@ -10,7 +10,7 @@ module.exports = function(grunt) {
     'clean:release',
     'clean:release',
     'copy:public_to_gen',
     'copy:public_to_gen',
     'typescript:build',
     'typescript:build',
-    // 'karma:test',
+    'karma:test',
     'phantomjs',
     'phantomjs',
     'css',
     'css',
     'htmlmin:build',
     'htmlmin:build',

+ 3 - 1
tasks/options/concat.js

@@ -28,8 +28,10 @@ module.exports = function(config) {
 
 
     js: {
     js: {
       src: [
       src: [
+        '<%= genDir %>/vendor/npm/es5-shim/es5-shim.js',
         '<%= genDir %>/vendor/npm/es6-shim/es6-shim.js',
         '<%= genDir %>/vendor/npm/es6-shim/es6-shim.js',
-        '<%= genDir %>/vendor/npm/es6-promise/es6-promise.js',
+        '<%= genDir %>/vendor/npm/es6-promise/dist/es6-promise.js',
+        '<%= genDir %>/vendor/npm/systemjs/dist/system-polyfills.js',
         '<%= genDir %>/vendor/npm/systemjs/dist/system.js',
         '<%= genDir %>/vendor/npm/systemjs/dist/system.js',
         '<%= genDir %>/app/system.conf.js',
         '<%= genDir %>/app/system.conf.js',
         '<%= genDir %>/app/boot.js',
         '<%= genDir %>/app/boot.js',

+ 60 - 52
vendor/phantomjs/render.js

@@ -1,55 +1,63 @@
-var page = require('webpage').create();
-var args = require('system').args;
-var params = {};
-var regexp = /^([^=]+)=([^$]+)/;
-
-args.forEach(function(arg) {
-  var parts = arg.match(regexp);
-  if (!parts) { return; }
-  params[parts[1]] = parts[2];
-});
-
-var usage = "url=<url> png=<filename> width=<width> height=<height> cookiename=<cookiename> sessionid=<sessionid> domain=<domain>";
-
-if (!params.url || !params.png || !params.cookiename || ! params.sessionid || !params.domain) {
-  console.log(usage);
-  phantom.exit();
-}
-
-phantom.addCookie({
-  'name': params.cookiename,
-  'value': params.sessionid,
-  'domain': params.domain
-});
-
-page.viewportSize = {
-  width: params.width || '800',
-  height: params.height || '400'
-};
-
-var tries = 0;
-
-page.open(params.url, function (status) {
-  console.log('Loading a web page: ' + params.url);
-
-  function checkIsReady() {
-    var canvas = page.evaluate(function() {
-      var body = angular.element(document.body);   // 1
-      var rootScope = body.scope().$root;
-      var panelsToLoad = angular.element('div.panel').length;
-      return rootScope.performance.panelsRendered >= panelsToLoad;
-    });
-
-    if (canvas || tries === 1000) {
-      page.render(params.png);
-      phantom.exit();
-    }
-    else {
-      tries++;
-      setTimeout(checkIsReady, 10);
-    }
+(function() {
+  'use strict';
+
+  var page = require('webpage').create();
+  var args = require('system').args;
+  var params = {};
+  var regexp = /^([^=]+)=([^$]+)/;
+
+  args.forEach(function(arg) {
+    var parts = arg.match(regexp);
+    if (!parts) { return; }
+    params[parts[1]] = parts[2];
+  });
+
+  var usage = "url=<url> png=<filename> width=<width> height=<height> cookiename=<cookiename> sessionid=<sessionid> domain=<domain>";
+
+  if (!params.url || !params.png || !params.cookiename || ! params.sessionid || !params.domain) {
+    console.log(usage);
+    phantom.exit();
   }
   }
 
 
-  setTimeout(checkIsReady, 200);
+  phantom.addCookie({
+    'name': params.cookiename,
+    'value': params.sessionid,
+    'domain': params.domain
+  });
+
+  page.viewportSize = {
+    width: params.width || '800',
+    height: params.height || '400'
+  };
+
+  var tries = 0;
+
+  page.open(params.url, function (status) {
+    console.log('Loading a web page: ' + params.url + ' status: ' + status);
+
+    function checkIsReady() {
+      var canvas = page.evaluate(function() {
+        if (!window.angular) { return false; }
+        var body = window.angular.element(document.body);   // 1
+        if (!body.scope) { return false; }
+
+        var rootScope = body.scope();
+        if (!rootScope) {return false;}
+        if (!rootScope.performance) { return false; }
+        var panelsToLoad = window.angular.element('div.panel').length;
+        return rootScope.performance.panelsRendered >= panelsToLoad;
+      });
+
+      if (canvas || tries === 1000) {
+        page.render(params.png);
+        phantom.exit();
+      }
+      else {
+        tries++;
+        setTimeout(checkIsReady, 10);
+      }
+    }
 
 
-});
+    setTimeout(checkIsReady, 200);
+  });
+})();