Просмотр исходного кода

Merge branch 'master' into panelbase

Torkel Ödegaard 10 лет назад
Родитель
Сommit
7b4fe824ec

+ 5 - 2
CHANGELOG.md

@@ -1,6 +1,6 @@
 # 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)
 * **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)
@@ -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
 **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)
 * **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)
 
+### 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)
 
 ### 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).
 
+> 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 then use this variable in your Prometheus metric queries.
 

+ 51 - 12
pkg/api/api_plugin.go

@@ -1,13 +1,17 @@
 package api
 
 import (
+	"bytes"
 	"encoding/json"
+	"fmt"
 	"net/http"
 	"net/http/httputil"
 	"net/url"
+	"text/template"
 
 	"gopkg.in/macaron.v1"
 
+	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/middleware"
 	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, ApiPlugin(route.Url))
+			handlers = append(handlers, ApiPlugin(route, plugin.IncludedInAppId))
 			r.Route(url, route.Method, handlers...)
 			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) {
 		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.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) {
+
 		req.URL.Scheme = targetUrl.Scheme
 		req.URL.Host = targetUrl.Host
 		req.Host = targetUrl.Host
@@ -69,7 +69,46 @@ func NewApiPluginProxy(ctx string, proxyPath string, targetUrl *url.URL) *httput
 		// clear cookie headers
 		req.Header.Del("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}

+ 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.Pinned = data.Pinned
 		dto.Info = &def.Info
+		dto.JsonData = data.JsonData
 	}
 
 	return dto

+ 14 - 1
pkg/models/app_settings.go

@@ -1,6 +1,13 @@
 package models
 
-import "time"
+import (
+	"errors"
+	"time"
+)
+
+var (
+	ErrAppSettingNotFound = errors.New("AppSetting not found")
+)
 
 type AppSettings struct {
 	Id       int64
@@ -33,3 +40,9 @@ type GetAppSettingsQuery struct {
 	OrgId  int64
 	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
 	return nil
 }

+ 0 - 16
pkg/plugins/models.go

@@ -2,8 +2,6 @@ package plugins
 
 import (
 	"encoding/json"
-
-	"github.com/grafana/grafana/pkg/models"
 )
 
 type PluginLoader interface {
@@ -44,20 +42,6 @@ type PluginStaticRoute struct {
 	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 {
 	Panels      []*PanelPlugin
 	DataSources map[string]*DataSourcePlugin

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

@@ -9,6 +9,7 @@ import (
 
 func init() {
 	bus.AddHandler("sql", GetAppSettings)
+	bus.AddHandler("sql", GetAppSettingByAppId)
 	bus.AddHandler("sql", UpdateAppSettings)
 }
 
@@ -19,6 +20,18 @@ func GetAppSettings(query *m.GetAppSettingsQuery) error {
 	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 {
 	return inTransaction2(func(sess *session) error {
 		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,'-');
   };
 
-  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) {
     if (str[0] !== '/') {
       return new RegExp('^' + str + '$');

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

@@ -98,6 +98,8 @@
 		<div class="simple-box-body">
 			<div ng-if="ctrl.appModel.appId">
 				<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>
 	</section>

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

@@ -7,6 +7,7 @@ export class SubmenuCtrl {
   variables: any;
   dashboard: any;
 
+  /** @ngInject */
   constructor(private $rootScope, private templateValuesSrv, private dynamicDashboardSrv) {
     this.annotations = 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();
-    $rootScope.onAppEvent('dash-links-updated', updateDashLinks, $rootScope);
+    $rootScope.onAppEvent('dash-links-updated', updateDashLinks, $scope);
   });
 
   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.moveDashboard(playlistItem, -1);
+      $scope.movePlaylistItem(playlistItem, -1);
     };
 
     $scope.movePlaylistItemDown = function(playlistItem) {
-      $scope.moveDashboard(playlistItem, 1);
+      $scope.movePlaylistItem(playlistItem, 1);
     };
 
     $scope.init();

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

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

+ 1 - 0
public/app/grafana.ts

@@ -1,6 +1,7 @@
 ///<reference path="headers/common.d.ts" />
 
 import 'bootstrap';
+import 'vendor/filesaver';
 import 'lodash-src';
 import 'angular-strap';
 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 {
 
+  /** @ngInject */
   constructor(private $q, private datasourceSrv) {
   }
 

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

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

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

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

+ 1 - 0
public/less/sidemenu.less

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

+ 2 - 0
public/views/index.html

@@ -56,8 +56,10 @@
 	</script>
 
 	<!-- 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-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/app/system.conf.js"></script>
 	<script src="[[.AppSubUrl]]/public/app/boot.js"></script>

+ 3 - 1
symlink_git_hooks.sh

@@ -1,3 +1,5 @@
 #/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',
     'copy:public_to_gen',
     'typescript:build',
-    // 'karma:test',
+    'karma:test',
     'phantomjs',
     'css',
     'htmlmin:build',

+ 3 - 1
tasks/options/concat.js

@@ -28,8 +28,10 @@ module.exports = function(config) {
 
     js: {
       src: [
+        '<%= genDir %>/vendor/npm/es5-shim/es5-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 %>/app/system.conf.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);
+  });
+})();