From b6de656cf1d783b1a3de83b3da70247d0e898020 Mon Sep 17 00:00:00 2001 From: Nathan LaFreniere Date: Thu, 14 Apr 2016 17:08:54 -0700 Subject: [PATCH 01/47] dont quote numbers for influxdb, closes #4163 --- public/app/plugins/datasource/influxdb/influx_query.ts | 4 +++- public/app/plugins/datasource/influxdb/query_builder.js | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/datasource/influxdb/influx_query.ts b/public/app/plugins/datasource/influxdb/influx_query.ts index 85457c7a8b3..8143e187b76 100644 --- a/public/app/plugins/datasource/influxdb/influx_query.ts +++ b/public/app/plugins/datasource/influxdb/influx_query.ts @@ -152,7 +152,9 @@ export default class InfluxQuery { if (interpolate) { value = this.templateSrv.replace(value, this.scopedVars); } - value = "'" + value.replace('\\', '\\\\') + "'"; + if (isNaN(+value)) { + value = "'" + value.replace('\\', '\\\\') + "'"; + } } else if (interpolate){ value = this.templateSrv.replace(value, this.scopedVars, 'regex'); } diff --git a/public/app/plugins/datasource/influxdb/query_builder.js b/public/app/plugins/datasource/influxdb/query_builder.js index 1921c672ba6..07d920350d7 100644 --- a/public/app/plugins/datasource/influxdb/query_builder.js +++ b/public/app/plugins/datasource/influxdb/query_builder.js @@ -25,8 +25,8 @@ function (_) { } } - // quote value unless regex - if (operator !== '=~' && operator !== '!~') { + // quote value unless regex or number + if (operator !== '=~' && operator !== '!~' && isNaN(+value)) { value = "'" + value + "'"; } From 7e66d0bcc1b2a5049e1f5229f23770dff758851e Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 18 Apr 2016 15:25:06 +0200 Subject: [PATCH 02/47] fix(): remove only usage in tests --- public/test/core/utils/emitter_specs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/test/core/utils/emitter_specs.ts b/public/test/core/utils/emitter_specs.ts index f7076c46719..fec4d02a649 100644 --- a/public/test/core/utils/emitter_specs.ts +++ b/public/test/core/utils/emitter_specs.ts @@ -24,7 +24,7 @@ describe("Emitter", () => { expect(sub2Called).to.be(true); }); - it.only('should handle errors', () => { + it('should handle errors', () => { var events = new Emitter(); var sub1Called = 0; var sub2Called = 0; From fde6eee4f4ac9326ccad44059ef9ffff2927204c Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 18 Apr 2016 16:17:03 +0200 Subject: [PATCH 03/47] fix(influxdb): adds support for multi table values When quering for tag values without measurement all tags and values should be shown closes #4726 --- .../datasource/influxdb/response_parser.ts | 28 +++++++++++++------ .../influxdb/specs/response_parser_specs.ts | 26 +++++++++++------ 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/public/app/plugins/datasource/influxdb/response_parser.ts b/public/app/plugins/datasource/influxdb/response_parser.ts index 2e33b398a88..ec0b1a4c249 100644 --- a/public/app/plugins/datasource/influxdb/response_parser.ts +++ b/public/app/plugins/datasource/influxdb/response_parser.ts @@ -12,17 +12,27 @@ export default class ResponseParser { return []; } - var series = influxResults.series[0]; - return _.map(series.values, (value) => { - if (_.isArray(value)) { - if (query.toLowerCase().indexOf('show tag values') >= 0) { - return { text: (value[1] || value[0]) }; + var res = []; + _.each(influxResults.series, (serie) => { + _.each(serie.values, (value) => { + if (_.isArray(value)) { + if (query.toLowerCase().indexOf('show tag values') >= 0) { + addUnique(res, { text: (value[1] || value[0])}); + } else { + addUnique(res, { text: value[0]}); + } } else { - return { text: value[0] }; + addUnique(res, {text: value}); } - } else { - return { text: value }; - } + }); }); + + return res; + } +} + +function addUnique(arr, value) { + if (!_.any(arr, value)) { + arr.push(value); } } diff --git a/public/app/plugins/datasource/influxdb/specs/response_parser_specs.ts b/public/app/plugins/datasource/influxdb/specs/response_parser_specs.ts index e58fe32dd1b..f545753d10e 100644 --- a/public/app/plugins/datasource/influxdb/specs/response_parser_specs.ts +++ b/public/app/plugins/datasource/influxdb/specs/response_parser_specs.ts @@ -38,7 +38,7 @@ describe("influxdb response parser", () => { { "name": "hostnameTagValues", "columns": ["hostname"], - "values": [ ["server1"], ["server2"] ] + "values": [ ["server1"], ["server2"], ["server2"] ] } ] } @@ -54,7 +54,7 @@ describe("influxdb response parser", () => { }); }); - describe("response from 0.11.0", () => { + describe("response from 0.12.0", () => { var response = { "results": [ { @@ -62,8 +62,19 @@ describe("influxdb response parser", () => { { "name": "cpu", "columns": [ "key", "value"], - "values": [ [ "source", "site" ], [ "source", "api" ] ] - } + "values": [ + [ "source", "site" ], + [ "source", "api" ] + ] + }, + { + "name": "logins", + "columns": [ "key", "value"], + "values": [ + [ "source", "site" ], + [ "source", "webapi"] + ] + }, ] } ] @@ -72,15 +83,12 @@ describe("influxdb response parser", () => { var result = this.parser.parse(query, response); it("should get two responses", () => { - expect(_.size(result)).to.be(2); + expect(_.size(result)).to.be(3); expect(result[0].text).to.be('site'); expect(result[1].text).to.be('api'); + expect(result[2].text).to.be('webapi'); }); }); - - - - }); describe("SHOW FIELD response", () => { From 80818f80a99ab89fd4fbf85182200e5aa4109573 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 18 Apr 2016 17:12:53 +0200 Subject: [PATCH 04/47] tech(grunt): add check for not including "only" in tests --- public/test/core/utils/emitter_specs.ts | 2 +- tasks/default_task.js | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/public/test/core/utils/emitter_specs.ts b/public/test/core/utils/emitter_specs.ts index f7076c46719..fec4d02a649 100644 --- a/public/test/core/utils/emitter_specs.ts +++ b/public/test/core/utils/emitter_specs.ts @@ -24,7 +24,7 @@ describe("Emitter", () => { expect(sub2Called).to.be(true); }); - it.only('should handle errors', () => { + it('should handle errors', () => { var events = new Emitter(); var sub1Called = 0; var sub2Called = 0; diff --git a/tasks/default_task.js b/tasks/default_task.js index d2bebb89789..2aa0a7d8826 100644 --- a/tasks/default_task.js +++ b/tasks/default_task.js @@ -25,6 +25,19 @@ module.exports = function(grunt) { 'typescript:build' ]); - grunt.registerTask('test', ['default', 'karma:test']); + grunt.registerTask('test', ['default', 'karma:test', 'no-only-tests']); + grunt.registerTask('no-only-tests', function() { + var files = grunt.file.expand('public/**/*_specs\.ts', 'public/**/*_specs\.js'); + + files.forEach(function(spec) { + var rows = grunt.file.read(spec).split('\n'); + rows.forEach(function(row) { + if (row.indexOf('.only(') > 0) { + grunt.log.errorlns(row); + grunt.fail.warn('found only statement in test: ' + spec) + } + }); + }); + }); }; From 267ab822c2650f19f86e1c46460bf027cec98ef7 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 18 Apr 2016 18:46:50 +0200 Subject: [PATCH 05/47] tech(influxdb): uses hashmap for uniqueness --- .../datasource/influxdb/response_parser.ts | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/public/app/plugins/datasource/influxdb/response_parser.ts b/public/app/plugins/datasource/influxdb/response_parser.ts index ec0b1a4c249..cb1efea6b33 100644 --- a/public/app/plugins/datasource/influxdb/response_parser.ts +++ b/public/app/plugins/datasource/influxdb/response_parser.ts @@ -12,27 +12,29 @@ export default class ResponseParser { return []; } - var res = []; - _.each(influxResults.series, (serie) => { - _.each(serie.values, (value) => { + var newInfluxdbFormat = query.toLowerCase().indexOf('show tag values') >= 0; + + var res = {}; + _.each(influxResults.series, serie => { + _.each(serie.values, value => { if (_.isArray(value)) { - if (query.toLowerCase().indexOf('show tag values') >= 0) { - addUnique(res, { text: (value[1] || value[0])}); + if (newInfluxdbFormat) { + addUnique(res, value[1] || value[0]); } else { - addUnique(res, { text: value[0]}); + addUnique(res, value[0]); } } else { - addUnique(res, {text: value}); + addUnique(res, value); } }); }); - return res; + return _.map(res, value => { + return { text: value}; + }); } } function addUnique(arr, value) { - if (!_.any(arr, value)) { - arr.push(value); - } + arr[value] = value; } From c148e54bbd7347c911f6220a8aa2368ccd9fb195 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 18 Apr 2016 18:52:09 +0200 Subject: [PATCH 06/47] style(influxdb): improve naming --- public/app/plugins/datasource/influxdb/response_parser.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/influxdb/response_parser.ts b/public/app/plugins/datasource/influxdb/response_parser.ts index cb1efea6b33..23173674361 100644 --- a/public/app/plugins/datasource/influxdb/response_parser.ts +++ b/public/app/plugins/datasource/influxdb/response_parser.ts @@ -12,13 +12,13 @@ export default class ResponseParser { return []; } - var newInfluxdbFormat = query.toLowerCase().indexOf('show tag values') >= 0; + var influxdb11format = query.toLowerCase().indexOf('show tag values') >= 0; var res = {}; _.each(influxResults.series, serie => { _.each(serie.values, value => { if (_.isArray(value)) { - if (newInfluxdbFormat) { + if (influxdb11format) { addUnique(res, value[1] || value[0]); } else { addUnique(res, value[0]); From bd21a08bc9c25dc7c03643ec0d91a22c0deab932 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 18 Apr 2016 17:33:51 -0400 Subject: [PATCH 07/47] fix(templating): fixed encoding of all value for regex, and custom all value, fixes #4755, fixes #4736 --- CHANGELOG.md | 7 +++++++ public/app/features/templating/templateSrv.js | 6 +++++- public/test/specs/templateSrv-specs.js | 7 ++++++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b8239bd437..0c57bfcfee2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# 3.0.0-beta6 (unreleased) + +### Bug fixes +* **InfluxDB 0.12**: Fixed issue templating and `show tag values` query only returning tags for first measurement, fixes [#4726](https://github.com/grafana/grafana/issues/4726) +* **Templating**: Fixed issue with regex formating when matching multiple values, fixes [#4755](https://github.com/grafana/grafana/issues/4755) +* **Templating**: Fixed issue with custom all value and escaping, fixes [#4736](https://github.com/grafana/grafana/issues/4736) + # 3.0.0-beta5 (2016-04-15) ### Bug fixes diff --git a/public/app/features/templating/templateSrv.js b/public/app/features/templating/templateSrv.js index 26dc77d36a9..f60414eac43 100644 --- a/public/app/features/templating/templateSrv.js +++ b/public/app/features/templating/templateSrv.js @@ -57,7 +57,7 @@ function (angular, _) { } var escapedValues = _.map(value, regexEscape); - return escapedValues.join('|'); + return '(' + escapedValues.join('|') + ')'; } case "lucene": { if (typeof value === 'string') { @@ -152,6 +152,10 @@ function (angular, _) { value = variable.current.value; if (self.isAllValue(value)) { value = self.getAllValue(variable); + // skip formating of custom all values + if (variable.allValue) { + return value; + } } var res = self.formatValue(value, format, variable); diff --git a/public/test/specs/templateSrv-specs.js b/public/test/specs/templateSrv-specs.js index 4948e1272ce..3334d7a7bfe 100644 --- a/public/test/specs/templateSrv-specs.js +++ b/public/test/specs/templateSrv-specs.js @@ -99,6 +99,11 @@ define([ var target = _templateSrv.replace('this.$test.filters', {}, 'glob'); expect(target).to.be('this.*.filters'); }); + + it('should not escape custom all value', function() { + var target = _templateSrv.replace('this.$test', {}, 'regex'); + expect(target).to.be('this.*'); + }); }); describe('lucene format', function() { @@ -127,7 +132,7 @@ define([ it('multi value and regex format should render regex string', function() { var result = _templateSrv.formatValue(['test.','test2'], 'regex'); - expect(result).to.be('test\\.|test2'); + expect(result).to.be('(test\\.|test2)'); }); it('multi value and pipe should render pipe string', function() { From 485a3778009fdffe974273f7f19b3471cb37f1f0 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 19 Apr 2016 08:27:26 +0200 Subject: [PATCH 08/47] fix(cli): adds support for local plugin folder closes #4572 --- pkg/cmd/grafana-cli/main.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/cmd/grafana-cli/main.go b/pkg/cmd/grafana-cli/main.go index 5cc3fb6f306..b9549e00c1a 100644 --- a/pkg/cmd/grafana-cli/main.go +++ b/pkg/cmd/grafana-cli/main.go @@ -8,7 +8,6 @@ import ( "github.com/codegangsta/cli" "github.com/grafana/grafana/pkg/cmd/grafana-cli/commands" "github.com/grafana/grafana/pkg/cmd/grafana-cli/log" - "strings" ) var version = "master" @@ -18,7 +17,7 @@ func getGrafanaPluginDir() string { defaultNix := "/var/lib/grafana/plugins" if currentOS == "windows" { - return "C:\\opt\\grafana\\plugins" + return "../data/plugins" } pwd, err := os.Getwd() @@ -29,16 +28,17 @@ func getGrafanaPluginDir() string { } if isDevenvironment(pwd) { - return "../../../data/plugins" + return "../data/plugins" } return defaultNix } func isDevenvironment(pwd string) bool { - // if grafana-cli is executed from the cmd folder we can assume + // if ../conf/defaults.ini exists, grafana is not installed as package // that its in development environment. - return strings.HasSuffix(pwd, "/pkg/cmd/grafana-cli") + _, err := os.Stat("../conf/defaults.ini") + return err == nil } func main() { From 5abaf26b5f529fe196e3fa112b3fd34b0614e159 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 19 Apr 2016 08:32:23 +0200 Subject: [PATCH 09/47] style(cli): remove some logging --- pkg/cmd/grafana-cli/commands/remove_command.go | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/pkg/cmd/grafana-cli/commands/remove_command.go b/pkg/cmd/grafana-cli/commands/remove_command.go index e0ecbb2b788..9792ed9d095 100644 --- a/pkg/cmd/grafana-cli/commands/remove_command.go +++ b/pkg/cmd/grafana-cli/commands/remove_command.go @@ -3,7 +3,7 @@ package commands import ( "errors" - "github.com/grafana/grafana/pkg/cmd/grafana-cli/log" + "fmt" m "github.com/grafana/grafana/pkg/cmd/grafana-cli/models" services "github.com/grafana/grafana/pkg/cmd/grafana-cli/services" ) @@ -15,22 +15,17 @@ func removeCommand(c CommandLine) error { pluginPath := c.GlobalString("pluginsDir") localPlugins := getPluginss(pluginPath) - log.Info("remove!\n") - plugin := c.Args().First() - log.Info("plugin: " + plugin + "\n") if plugin == "" { return errors.New("Missing plugin parameter") } - log.Infof("plugins : \n%v\n", localPlugins) - for _, p := range localPlugins { if p.Id == c.Args().First() { - log.Infof("removing plugin %s", p.Id) removePlugin(pluginPath, p.Id) + return nil } } - return nil + return fmt.Errorf("Could not find plugin named %s", c.Args().First()) } From 903d1b77970a08ab3f527b355c60c3388f34c6a6 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 19 Apr 2016 09:13:58 +0200 Subject: [PATCH 10/47] tech(cli): dont use defer statements in loops --- pkg/cmd/grafana-cli/commands/install_command.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/grafana-cli/commands/install_command.go b/pkg/cmd/grafana-cli/commands/install_command.go index 81e7a15233b..b837c1990c1 100644 --- a/pkg/cmd/grafana-cli/commands/install_command.go +++ b/pkg/cmd/grafana-cli/commands/install_command.go @@ -164,14 +164,14 @@ func downloadFile(pluginName, filePath, url string) (err error) { return fmt.Errorf(permissionsDeniedMessage, newFile) } - defer dst.Close() src, err := zf.Open() if err != nil { log.Errorf("%v", err) } - defer src.Close() io.Copy(dst, src) + dst.Close() + src.Close() } } From a8c68e33db48dfd01b03a11c165c94b6a56aaf57 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 19 Apr 2016 10:22:02 +0200 Subject: [PATCH 11/47] feat(cli): add more logging for failed install --- pkg/cmd/grafana-cli/commands/install_command.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/grafana-cli/commands/install_command.go b/pkg/cmd/grafana-cli/commands/install_command.go index b837c1990c1..409604a7c2e 100644 --- a/pkg/cmd/grafana-cli/commands/install_command.go +++ b/pkg/cmd/grafana-cli/commands/install_command.go @@ -126,8 +126,8 @@ func downloadFile(pluginName, filePath, url string) (err error) { defer func() { if r := recover(); r != nil { retryCount++ - if retryCount == 1 { - log.Debug("\nFailed downloading. Will retry once.\n") + if retryCount < 3 { + fmt.Printf("\nFailed downloading. Will retry once.\n%v\n", r) downloadFile(pluginName, filePath, url) } else { panic(r) From d38d4efc18d67dd8ab4b466dcaf65d46a403f725 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 19 Apr 2016 10:39:55 +0200 Subject: [PATCH 12/47] style(cli): improve logging to find install crash --- pkg/cmd/grafana-cli/commands/install_command.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/grafana-cli/commands/install_command.go b/pkg/cmd/grafana-cli/commands/install_command.go index 409604a7c2e..c528cbfc5bd 100644 --- a/pkg/cmd/grafana-cli/commands/install_command.go +++ b/pkg/cmd/grafana-cli/commands/install_command.go @@ -166,7 +166,7 @@ func downloadFile(pluginName, filePath, url string) (err error) { src, err := zf.Open() if err != nil { - log.Errorf("%v", err) + log.Errorf("%Failed to open zipfile: v", err) } io.Copy(dst, src) From 4e8396dbcea9907f9b819f529b146dfbd9f1cc7d Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 19 Apr 2016 13:41:35 +0200 Subject: [PATCH 13/47] fix(graph): sets legend table scroll to auto closes #4760 --- public/sass/components/_panel_graph.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/sass/components/_panel_graph.scss b/public/sass/components/_panel_graph.scss index ca79034a0bd..b830561f816 100644 --- a/public/sass/components/_panel_graph.scss +++ b/public/sass/components/_panel_graph.scss @@ -85,7 +85,8 @@ } .graph-legend-table { - overflow-y: scroll; + overflow-y: auto; + overflow-x: hidden; .graph-legend-series { display: table-row; From ff22f430024d87dacbfbd75ad86fd9f0475b955d Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 19 Apr 2016 14:46:03 +0200 Subject: [PATCH 14/47] fix(cli): fixes missplaced % --- pkg/cmd/grafana-cli/commands/install_command.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/grafana-cli/commands/install_command.go b/pkg/cmd/grafana-cli/commands/install_command.go index c528cbfc5bd..addcf9a8b7e 100644 --- a/pkg/cmd/grafana-cli/commands/install_command.go +++ b/pkg/cmd/grafana-cli/commands/install_command.go @@ -166,7 +166,7 @@ func downloadFile(pluginName, filePath, url string) (err error) { src, err := zf.Open() if err != nil { - log.Errorf("%Failed to open zipfile: v", err) + log.Errorf("Failed to extract file: %v", err) } io.Copy(dst, src) From 44c1d8c1e54a9ca0338edaa65825b96798e802cf Mon Sep 17 00:00:00 2001 From: David Keijser Date: Fri, 8 Jan 2016 18:55:49 +0100 Subject: [PATCH 15/47] Add jquery.flot.gauge --- public/vendor/flot/jquery.flot.gauge.js | 955 ++++++++++++++++++++++++ 1 file changed, 955 insertions(+) create mode 100644 public/vendor/flot/jquery.flot.gauge.js diff --git a/public/vendor/flot/jquery.flot.gauge.js b/public/vendor/flot/jquery.flot.gauge.js new file mode 100644 index 00000000000..02aa553b7fa --- /dev/null +++ b/public/vendor/flot/jquery.flot.gauge.js @@ -0,0 +1,955 @@ +/*! + * jquery.flot.gauge v1.1.0 * + * + * Flot plugin for rendering gauge charts. + * + * Copyright (c) 2015 @toyoty99. + * Licensed under the MIT license. + */ + +/** + * @module flot.gauge + */ +(function($) { + + + /** + * Gauge class + * + * @class Gauge + */ + var Gauge = (function() { + /** + * context of canvas + * + * @property context + * @type Object + */ + var context; + /** + * placeholder of canvas + * + * @property placeholder + * @type Object + */ + var placeholder; + /** + * options of plot + * + * @property options + * @type Object + */ + var options; + /** + * options of gauge + * + * @property gaugeOptions + * @type Object + */ + var gaugeOptions; + /** + * data series + * + * @property series + * @type Array + */ + var series; + /** + * logger + * + * @property logger + * @type Object + */ + var logger; + + /** + * constructor + * + * @class Gauge + * @constructor + * @param {Object} gaugeOptions gauge options + */ + var Gauge = function(plot, ctx) { + context = ctx; + placeholder = plot.getPlaceholder(); + options = plot.getOptions(); + gaugeOptions = options.series.gauges; + series = plot.getData(); + logger = getLogger(gaugeOptions.debug); + } + + /** + * calculate layout + * + * @method calculateLayout + * @return the calculated layout properties + */ + Gauge.prototype.calculateLayout = function() { + + var canvasWidth = placeholder.width(); + var canvasHeight = placeholder.height(); + + + + // calculate cell size + var columns = Math.min(series.length, gaugeOptions.layout.columns); + var rows = Math.ceil(series.length / columns); + + + + var margin = gaugeOptions.layout.margin; + var hMargin = gaugeOptions.layout.hMargin; + var vMargin = gaugeOptions.layout.vMargin; + var cellWidth = (canvasWidth - (margin * 2) - (hMargin * (columns - 1))) / columns; + var cellHeight = (canvasHeight - (margin * 2) - (vMargin * (rows - 1))) / rows; + if (gaugeOptions.layout.square) { + var cell = Math.min(cellWidth, cellHeight); + cellWidth = cell; + cellHeight = cell; + } + + + + // calculate 'auto' values + calculateAutoValues(gaugeOptions, cellWidth); + + // calculate maximum radius + var cellMargin = gaugeOptions.cell.margin; + var labelMargin = 0; + var labelFontSize = 0; + if (gaugeOptions.label.show) { + labelMargin = gaugeOptions.label.margin; + labelFontSize = gaugeOptions.label.font.size; + } + var valueMargin = 0; + var valueFontSize = 0; + if (gaugeOptions.value.show) { + valueMargin = gaugeOptions.value.margin; + valueFontSize = gaugeOptions.value.font.size; + } + var thresholdWidth = 0; + if (gaugeOptions.threshold.show) { + thresholdWidth = gaugeOptions.threshold.width; + } + var thresholdLabelMargin = 0; + var thresholdLabelFontSize = 0; + if (gaugeOptions.threshold.label.show) { + thresholdLabelMargin = gaugeOptions.threshold.label.margin; + thresholdLabelFontSize = gaugeOptions.threshold.label.font.size; + } + + var maxRadiusH = (cellWidth / 2) - cellMargin - thresholdWidth - (thresholdLabelMargin * 2) - thresholdLabelFontSize; + + var startAngle = gaugeOptions.gauge.startAngle; + var endAngle = gaugeOptions.gauge.endAngle; + var dAngle = (endAngle - startAngle) / 100; + var heightRatioV = -1; + for (var a = startAngle; a < endAngle; a += dAngle) { + heightRatioV = Math.max(heightRatioV, Math.sin(toRad(a))); + } + heightRatioV = Math.max(heightRatioV, Math.sin(toRad(endAngle))); + var outerRadiusV = (cellHeight - (cellMargin * 2) - (labelMargin * 2) - labelFontSize) / (1 + heightRatioV); + if (outerRadiusV * heightRatioV < valueMargin + (valueFontSize / 2)) { + outerRadiusV = cellHeight - (cellMargin * 2) - (labelMargin * 2) - labelFontSize - valueMargin - (valueFontSize / 2); + } + var maxRadiusV = outerRadiusV - (thresholdLabelMargin * 2) - thresholdLabelFontSize - thresholdWidth; + + var radius = Math.min(maxRadiusH, maxRadiusV); + + + var width = gaugeOptions.gauge.width; + if (width >= radius) { + width = Math.max(3, radius / 3); + } + + + var outerRadius = (thresholdLabelMargin * 2) + thresholdLabelFontSize + thresholdWidth + radius; + var gaugeOuterHeight = Math.max(outerRadius * (1 + heightRatioV), outerRadius + valueMargin + (valueFontSize / 2)); + + return { + canvasWidth: canvasWidth, + canvasHeight: canvasHeight, + margin: margin, + hMargin: hMargin, + vMargin: vMargin, + columns: columns, + rows: rows, + cellWidth: cellWidth, + cellHeight: cellHeight, + cellMargin: cellMargin, + labelMargin: labelMargin, + labelFontSize: labelFontSize, + valueMargin: valueMargin, + valueFontSize: valueFontSize, + width: width, + radius: radius, + thresholdWidth: thresholdWidth, + thresholdLabelMargin: thresholdLabelMargin, + thresholdLabelFontSize: thresholdLabelFontSize, + gaugeOuterHeight: gaugeOuterHeight + }; + } + + /** + * calculate the values which are set as 'auto' + * + * @method calculateAutoValues + * @param {Object} gaugeOptionsi the options of the gauge + * @param {Number} cellWidth the width of cell + */ + function calculateAutoValues(gaugeOptionsi, cellWidth) { + + if (gaugeOptionsi.gauge.width === "auto") { + gaugeOptionsi.gauge.width = Math.max(5, cellWidth / 8); + } + if (gaugeOptionsi.label.margin === "auto") { + gaugeOptionsi.label.margin = Math.max(1, cellWidth / 20); + } + if (gaugeOptionsi.label.font.size === "auto") { + gaugeOptionsi.label.font.size = Math.max(5, cellWidth / 8); + } + if (gaugeOptionsi.value.margin === "auto") { + gaugeOptionsi.value.margin = Math.max(1, cellWidth / 30); + } + if (gaugeOptionsi.value.font.size === "auto") { + gaugeOptionsi.value.font.size = Math.max(5, cellWidth / 9); + } + if (gaugeOptionsi.threshold.width === "auto") { + gaugeOptionsi.threshold.width = Math.max(3, cellWidth / 100); + } + if (gaugeOptionsi.threshold.label.margin === "auto") { + gaugeOptionsi.threshold.label.margin = Math.max(3, cellWidth / 40); + } + if (gaugeOptionsi.threshold.label.font.size === "auto") { + gaugeOptionsi.threshold.label.font.size = Math.max(5, cellWidth / 15); + } + + } + Gauge.prototype.calculateAutoValues = calculateAutoValues; + + /** + * calculate the layout of the cell inside + * + * @method calculateCellLayout + * @param {Object} gaugeOptionsi the options of the gauge + * @param {Number} cellWidth the width of cell + * @param {Number} i the index of the series + * @return the calculated cell layout properties + */ + Gauge.prototype.calculateCellLayout = function(gaugeOptionsi, layout, i) { + + // calculate top, left and center + var c = col(layout.columns, i); + var r = row(layout.columns, i); + var x = layout.margin + (layout.cellWidth + layout.hMargin) * c; + var y = layout.margin + (layout.cellHeight + layout.vMargin) * r; + var cx = x + (layout.cellWidth / 2); + var cy = y + layout.cellMargin + (layout.labelMargin * 2) + layout.labelFontSize + layout.thresholdWidth + + layout.thresholdLabelFontSize + (layout.thresholdLabelMargin * 2) + layout.radius; + var blank = layout.cellHeight - (layout.cellMargin * 2) - (layout.labelMargin * 2) - layout.labelFontSize - layout.gaugeOuterHeight; + var offsetY = 0; + if (gaugeOptionsi.cell.vAlign === "middle") { + offsetY = (blank / 2); + } else if (gaugeOptionsi.cell.vAlign === "bottom") { + offsetY = blank; + } + cy += offsetY; + + return { + col: c, + row: r, + x: x, + y: y, + offsetY: offsetY, + cellWidth: layout.cellWidth, + cellHeight: layout.cellHeight, + cellMargin: layout.cellMargin, + cx: cx, + cy: cy + } + } + + /** + * draw the background of chart + * + * @method drawBackground + * @param {Object} layout the layout properties + */ + Gauge.prototype.drawBackground = function(layout) { + + if (!options.grid.show) { + return; + } + context.save(); + context.strokeStyle = options.grid.borderColor; + context.lineWidth = options.grid.borderWidth; + context.strokeRect(0, 0, layout.canvasWidth, layout.canvasHeight); + if (options.grid.backgroundColor) { + context.fillStyle = options.grid.backgroundColor; + context.fillRect(0, 0, layout.canvasWidth, layout.canvasHeight); + } + context.restore(); + } + + /** + * draw the background of cell + * + * @method drawCellBackground + * @param {Object} gaugeOptionsi the options of the gauge + * @param {Object} cellLayout the cell layout properties + */ + Gauge.prototype.drawCellBackground = function(gaugeOptionsi, cellLayout) { + + context.save(); + if (gaugeOptionsi.cell.border && gaugeOptionsi.cell.border.color && gaugeOptionsi.cell.border.width) { + context.strokeStyle = gaugeOptionsi.cell.border.color; + context.lineWidth = gaugeOptionsi.cell.border.width; + context.strokeRect(cellLayout.x, cellLayout.y, cellLayout.cellWidth, cellLayout.cellHeight); + } + if (gaugeOptionsi.cell.background && gaugeOptionsi.cell.background.color) { + context.fillStyle = gaugeOptionsi.cell.background.color; + context.fillRect(cellLayout.x, cellLayout.y, cellLayout.cellWidth, cellLayout.cellHeight); + } + context.restore(); + } + + /** + * draw the gauge + * + * @method drawGauge + * @param {Object} gaugeOptionsi the options of the gauge + * @param {Object} layout the layout properties + * @param {Object} cellLayout the cell layout properties + * @param {String} label the label of data + * @param {Number} data the value of the gauge + */ + Gauge.prototype.drawGauge = function(gaugeOptionsi, layout, cellLayout, label, data) { + + + var blur = gaugeOptionsi.gauge.shadow.show ? gaugeOptionsi.gauge.shadow.blur : 0; + + + // draw gauge frame + drawArcWithShadow( + cellLayout.cx, // center x + cellLayout.cy, // center y + layout.radius, + layout.width, + toRad(gaugeOptionsi.gauge.startAngle), + toRad(gaugeOptionsi.gauge.endAngle), + gaugeOptionsi.gauge.stroke.color, // line color + gaugeOptionsi.gauge.stroke.width, // line width + gaugeOptionsi.gauge.frameColor, // fill color + blur); + + // draw gauge + var c1 = getColor(gaugeOptionsi, data); + var a2 = calculateAngle(gaugeOptionsi, layout, data); + drawArcWithShadow( + cellLayout.cx, // center x + cellLayout.cy, // center y + layout.radius - 1, + layout.width - 2, + toRad(gaugeOptionsi.gauge.startAngle), + toRad(a2), + c1, // line color + 1, // line width + c1, // fill color + blur); + } + + /** + * decide the color of the data from the threshold options + * + * @method getColor + * @private + * @param {Object} gaugeOptionsi the options of the gauge + * @param {Number} data the value of the gauge + */ + function getColor(gaugeOptionsi, data) { + var color; + for (var i = 0; i < gaugeOptionsi.threshold.values.length; i++) { + var threshold = gaugeOptionsi.threshold.values[i]; + color = threshold.color; + if (data <= threshold.value) { + break; + } + } + return color; + } + + /** + * calculate the angle of the data + * + * @method calculateAngle + * @private + * @param {Object} gaugeOptionsi the options of the gauge + * @param {Object} layout the layout properties + * @param {Number} data the value of the gauge + */ + function calculateAngle(gaugeOptionsi, layout, data) { + var a = + gaugeOptionsi.gauge.startAngle + + (gaugeOptionsi.gauge.endAngle - gaugeOptionsi.gauge.startAngle) + * ((data - gaugeOptionsi.gauge.min) / (gaugeOptionsi.gauge.max - gaugeOptionsi.gauge.min)); + + if (a < gaugeOptionsi.gauge.startAngle) { + a = gaugeOptionsi.gauge.startAngle; + } else if (a > gaugeOptionsi.gauge.endAngle) { + a = gaugeOptionsi.gauge.endAngle; + } + return a; + } + + /** + * draw the arc of the threshold + * + * @method drawThreshold + * @param {Object} gaugeOptionsi the options of the gauge + * @param {Object} layout the layout properties + * @param {Object} cellLayout the cell layout properties + */ + Gauge.prototype.drawThreshold = function(gaugeOptionsi, layout, cellLayout) { + + var a1 = gaugeOptionsi.gauge.startAngle; + for (var i = 0; i < gaugeOptionsi.threshold.values.length; i++) { + var threshold = gaugeOptionsi.threshold.values[i]; + c1 = threshold.color; + a2 = calculateAngle(gaugeOptionsi, layout, threshold.value); + drawArc( + context, + cellLayout.cx, // center x + cellLayout.cy, // center y + layout.radius + layout.thresholdWidth, + layout.thresholdWidth - 2, + toRad(a1), + toRad(a2), + c1, // line color + 1, // line width + c1); // fill color + a1 = a2; + } + } + + /** + * draw an arc with a shadow + * + * @method drawArcWithShadow + * @private + * @param {Number} cx the x position of the center + * @param {Number} cy the y position of the center + * @param {Number} r the radius of an arc + * @param {Number} w the width of an arc + * @param {Number} rd1 the start angle of an arc in radians + * @param {Number} rd2 the end angle of an arc in radians + * @param {String} lc the color of a line + * @param {Number} lw the widht of a line + * @param {String} fc the fill color of an arc + * @param {Number} blur the shdow blur + */ + function drawArcWithShadow(cx, cy, r, w, rd1, rd2, lc, lw, fc, blur) { + if (rd1 === rd2) { + return; + } + context.save(); + + drawArc(context, cx, cy, r, w, rd1, rd2, lc, lw, fc); + + if (blur) { + drawArc(context, cx, cy, r, w, rd1, rd2); + context.clip(); + context.shadowOffsetX = 0; + context.shadowOffsetY = 0; + context.shadowBlur = 10; + context.shadowColor = "gray"; + drawArc(context, cx, cy, r + 1, w + 2, rd1, rd2, lc, 1); + } + context.restore(); + } + + /** + * draw the label of the gauge + * + * @method drawLable + * @param {Object} gaugeOptionsi the options of the gauge + * @param {Object} layout the layout properties + * @param {Object} cellLayout the cell layout properties + * @param {Number} i the index of the series + * @param {Object} item the item of the series + */ + Gauge.prototype.drawLable = function(gaugeOptionsi, layout, cellLayout, i, item) { + + drawText( + cellLayout.cx, + cellLayout.y + cellLayout.cellMargin + layout.labelMargin + cellLayout.offsetY, + "flotGagueLabel" + i, + gaugeOptionsi.label.formatter ? gaugeOptionsi.label.formatter(item.label, item.data[0][1]) : text, + gaugeOptionsi.label); + } + + /** + * draw the value of the gauge + * + * @method drawValue + * @param {Object} gaugeOptionsi the options of the gauge + * @param {Object} layout the layout properties + * @param {Object} cellLayout the cell layout properties + * @param {Number} i the index of the series + * @param {Object} item the item of the series + */ + Gauge.prototype.drawValue = function(gaugeOptionsi, layout, cellLayout, i, item) { + + drawText( + cellLayout.cx, + cellLayout.cy - (gaugeOptionsi.value.font.size / 2), + "flotGagueValue" + i, + gaugeOptionsi.value.formatter ? gaugeOptionsi.value.formatter(item.label, item.data[0][1]) : text, + gaugeOptionsi.value); + } + + /** + * draw the values of the threshold + * + * @method drawThresholdValues + * @param {Object} gaugeOptionsi the options of the gauge + * @param {Object} layout the layout properties + * @param {Object} cellLayout the cell layout properties + * @param {Number} i the index of the series + */ + Gauge.prototype.drawThresholdValues = function(gaugeOptionsi, layout, cellLayout, i) { + + // min, max + drawThresholdValue(gaugeOptionsi, layout, cellLayout, "Min" + i, gaugeOptionsi.gauge.min, gaugeOptionsi.gauge.startAngle); + drawThresholdValue(gaugeOptionsi, layout, cellLayout, "Max" + i, gaugeOptionsi.gauge.max, gaugeOptionsi.gauge.endAngle); + // threshold values + for (var j = 0; j < gaugeOptionsi.threshold.values.length; j++) { + var threshold = gaugeOptionsi.threshold.values[j]; + if (threshold.value > gaugeOptionsi.gauge.min && threshold.value < gaugeOptionsi.gauge.max) { + var a = calculateAngle(gaugeOptionsi, layout, threshold.value); + drawThresholdValue(gaugeOptionsi, layout, cellLayout, i + "_" + j, threshold.value, a); + } + } + } + + /** + * draw the value of the threshold + * + * @method drawThresholdValue + * @param {Object} gaugeOptionsi the options of the gauge + * @param {Object} layout the layout properties + * @param {Object} cellLayout the cell layout properties + * @param {Number} i the index of the series + * @param {Number} value the value of the threshold + * @param {Number} a the angle of the value drawn + */ + function drawThresholdValue(gaugeOptionsi, layout, cellLayout, i, value, a) { + drawText( + cellLayout.cx + + ((layout.thresholdLabelMargin + (layout.thresholdLabelFontSize / 2) + layout.radius) + * Math.cos(toRad(a))), + cellLayout.cy + + ((layout.thresholdLabelMargin + (layout.thresholdLabelFontSize / 2) + layout.radius) + * Math.sin(toRad(a))), + "flotGagueThresholdValue" + i, + gaugeOptionsi.threshold.label.formatter ? gaugeOptionsi.threshold.label.formatter(value) : value, + gaugeOptionsi.threshold.label, + a); + } + + /** + * draw a text + * + * the textOptions is assumed as follows: + * + * textOptions: { + * background: { + * color: null, + * opacity: 0 + * }, + * font: { + * size: "auto" + * family: "\"MS ゴシック\",sans-serif" + * }, + * color: null + * } + * + * @method drawText + * @private + * @param {Number} x the x position of the text drawn (left top) + * @param {Number} y the y position of the text drawn (left top) + * @param {String} id the id of the dom element + * @param {String} text the text drawn + * @param {Object} textOptions the option of the text + * @param {Number} [a] the angle of the value drawn + */ + function drawText(x, y, id, text, textOptions, a) { + var span = $("#" + id); + var exists = span.length; + if (!exists) { + span = $("") + span.attr("id", id); + span.css("position", "absolute"); + span.css("top", y + "px"); + if (textOptions.font.size) { + span.css("font-size", textOptions.font.size + "px"); + } + if (textOptions.font.family) { + span.css("font-family", textOptions.font.family); + } + if (textOptions.color) { + span.css("color", textOptions.color); + } + if (textOptions.background.color) { + span.css("background-color", textOptions.background.color); + } + if (textOptions.background.opacity) { + span.css("opacity", textOptions.background.opacity); + } + placeholder.append(span); + } + span.text(text); + // after append, readjust the left position + span.css("left", x + "px"); // for redraw, resetting the left position is needed here + span.css("left", (parseInt(span.css("left")) - (span.width()/ 2)) + "px"); + + // at last, set angle + if (!exists && a) { + span.css("top", (parseInt(span.css("top")) - (span.height()/ 2)) + "px"); + span.css("transform", "rotate(" + ((180 * a) + 90) + "deg)"); // not supported for ie8 + } + } + + return Gauge; + })(); + + /** + * get a instance of Logger + * + * @method getLogger + * @for flot.gauge + * @private + * @param {Object} debugOptions the options of debug + */ + function getLogger(debugOptions) { + return typeof Logger !== "undefined" ? new Logger(debugOptions) : null; + } + + /** + * calculate the index of columns for the specified data + * + * @method col + * @for flot.gauge + * @param {Number} columns the number of columns + * @param {Number} i the index of the series + * @return the index of columns + */ + function col(columns, i) { + return i % columns; + } + + /** + * calculate the index of rows for the specified data + * + * @method row + * @for flot.gauge + * @param {Number} columns the number of rows + * @param {Number} i the index of the series + * @return the index of rows + */ + function row(columns, i) { + return Math.floor(i / columns); + } + + /** + * calculate the angle in radians + * + * internally, use a number without PI (0 - 2). + * so, in this function, multiply PI + * + * @method toRad + * @for flot.gauge + * @param {Number} a the number of angle without PI + * @return the angle in radians + */ + function toRad(a) { + return a * Math.PI; + } + + /** + * draw an arc + * + * @method drawArc + * @for flot.gauge + * @param {Object} context the context of canvas + * @param {Number} cx the x position of the center + * @param {Number} cy the y position of the center + * @param {Number} r the radius of an arc + * @param {Number} w the width of an arc + * @param {Number} rd1 the start angle of an arc in radians + * @param {Number} rd2 the end angle of an arc in radians + * @param {String} lc the color of a line + * @param {Number} lw the widht of a line + * @param {String} fc the fill color of an arc + */ + function drawArc(context, cx, cy, r, w, rd1, rd2, lc, lw, fc) { + if (rd1 === rd2) { + return; + } + var counterClockwise = false; + context.save(); + context.beginPath(); + context.arc(cx, cy, r, rd1, rd2, counterClockwise); + context.lineTo(cx + (r - w) * Math.cos(rd2), + cy + (r - w) * Math.sin(rd2)); + context.arc(cx, cy, r - w, rd2, rd1, !counterClockwise); + context.closePath(); + if (lw) { + context.lineWidth = lw; + } + if (lc) { + context.strokeStyle = lc; + context.stroke(); + } + if (fc) { + context.fillStyle = fc; + context.fill(); + } + context.restore(); + } + + /** + * initialize plugin + * + * @method init + * @for flot.gauge + * @private + * @param {Object} plot a instance of plot + */ + function init (plot) { + // add processOptions hook + plot.hooks.processOptions.push(function(plot, options) { + var logger = getLogger(options.series.gauges.debug); + + + + + // turn 'grid' and 'legend' off + if (options.series.gauges.show) { + options.grid.show = false; + options.legend.show = false; + } + + // sort threshold + var thresholds = options.series.gauges.threshold.values; + + thresholds.sort(function(a, b) { + if (a.value < b.value) { + return -1; + } else if (a.value > b.value) { + return 1; + } else { + return 0; + } + }); + + + + }); + + // add draw hook + plot.hooks.draw.push(function(plot, context) { + var options = plot.getOptions(); + var gaugeOptions = options.series.gauges; + + var logger = getLogger(gaugeOptions.debug); + + + if (!gaugeOptions.show) { + return; + } + + var series = plot.getData(); + + if (!series || !series.length) { + return; // if no series were passed + } + + var gauge = new Gauge(plot, context); + + // calculate layout + var layout = gauge.calculateLayout(); + + // debug layout + if (gaugeOptions.debug.layout) { + + } + + // draw background + gauge.drawBackground(layout) + + // draw cells (label, gauge, value, threshold) + for (var i = 0; i < series.length; i++) { + var item = series[i]; + + var gaugeOptionsi = $.extend({}, gaugeOptions, item.gauges); + if (item.gauges) { + // re-calculate 'auto' values + gauge.calculateAutoValues(gaugeOptionsi, layout.cellWidth); + } + + // calculate cell layout + var cellLayout = gauge.calculateCellLayout(gaugeOptionsi, layout, i); + + // draw cell background + gauge.drawCellBackground(gaugeOptionsi, cellLayout) + // debug layout + if (gaugeOptionsi.debug.layout) { + + } + // draw label + if (gaugeOptionsi.label.show) { + gauge.drawLable(gaugeOptionsi, layout, cellLayout, i, item); + } + // draw gauge + gauge.drawGauge(gaugeOptionsi, layout, cellLayout, item.label, item.data[0][1]); + // draw threshold + if (gaugeOptionsi.threshold.show) { + gauge.drawThreshold(gaugeOptionsi, layout, cellLayout); + } + if (gaugeOptionsi.threshold.label.show) { + gauge.drawThresholdValues(gaugeOptionsi, layout, cellLayout, i) + } + // draw value + if (gaugeOptionsi.value.show) { + gauge.drawValue(gaugeOptionsi, layout, cellLayout, i, item); + } + } + }); + } + + /** + * [defaults description] + * + * @property defaults + * @type {Object} + */ + var defaults = { + series: { + gauges: { + debug: { + log: false, + layout: false, + alert: false + }, + show: false, + layout: { + margin: 5, + columns: 3, + hMargin: 5, + vMargin: 5, + square: false + }, + cell: { + background: { + color: null + }, + border: { + color: "black", + width: 1 + }, + margin: 5, + vAlign: "middle" // 'top' or 'middle' or 'bottom' + }, + gauge: { + width: "auto", // a specified number, or 'auto' + startAngle: 0.9, // 0 - 2 factor of the radians + endAngle: 2.1, // 0 - 2 factor of the radians + min: 0, + max: 100, + shadow: { + show: true, + blur: 5 + }, + stroke: { + color: "lightgray", + width: 2 + }, + frameColor: "white" + }, + label: { + show: true, + margin: "auto", // a specified number, or 'auto' + background: { + color: null, + opacity: 0 + }, + font: { + size: "auto", // a specified number, or 'auto' + family: "sans-serif" + }, + color: null, + formatter: function(label, value) { + return label; + } + }, + value: { + show: true, + margin: "auto", // a specified number, or 'auto' + background: { + color: null, + opacity: 0 + }, + font: { + size: "auto", // a specified number, or 'auto' + family: "sans-serif" + }, + color: null, + formatter: function(label, value) { + return parseInt(value); + } + }, + threshold: { + show: true, + width: "auto", // a specified number, or 'auto' + label: { + show: true, + margin: "auto", // a specified number, or 'auto' + background: { + color: null, + opacity: 0 + }, + font: { + size: "auto", // a specified number, or 'auto' + family: ",sans-serif" + }, + color: null, + formatter: function(value) { + return value; + } + }, + values: [ + { + value: 50, + color: "lightgreen" + }, { + value: 80, + color: "yellow" + }, { + value: 100, + color: "red" + } + ] + } + } + } + }; + + // register the gauge plugin + $.plot.plugins.push({ + init: init, + options: defaults, + name: "gauge", + version: "1.1.0" + }); + +})(jQuery); From c455e501ac89a9e87eb977c81b45344eb2491266 Mon Sep 17 00:00:00 2001 From: David Keijser Date: Wed, 3 Feb 2016 17:47:46 +0100 Subject: [PATCH 16/47] Add gauge option to singlestat --- .../app/plugins/panel/singlestat/editor.html | 37 +++++++++ public/app/plugins/panel/singlestat/module.ts | 80 ++++++++++++++++++- public/app/system.conf.js | 3 +- public/test/test-main.js | 3 +- public/vendor/flot/jquery.flot.gauge.js | 29 ++++--- 5 files changed, 137 insertions(+), 15 deletions(-) diff --git a/public/app/plugins/panel/singlestat/editor.html b/public/app/plugins/panel/singlestat/editor.html index 6b0806133b8..437445a2400 100644 --- a/public/app/plugins/panel/singlestat/editor.html +++ b/public/app/plugins/panel/singlestat/editor.html @@ -156,6 +156,43 @@ +
+
+
+
    +
  • + Gauge +
  • +
  • + Show  + + +
  • +
  • + Threshold labels  + + +
  • +
  • + Min +
  • +
  • + +
  • +
  • + Max +
  • +
  • + +
  • +
+
+
+
+
+
diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index 8af684c9d68..19909f08441 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -4,6 +4,7 @@ import angular from 'angular'; import _ from 'lodash'; import $ from 'jquery'; import 'jquery.flot'; +import 'jquery.flot.gauge'; import kbn from 'app/core/utils/kbn'; import TimeSeries from 'app/core/time_series2'; @@ -38,6 +39,12 @@ var panelDefaults = { full: false, lineColor: 'rgb(31, 120, 193)', fillColor: 'rgba(31, 118, 189, 0.18)', + }, + gauge: { + show: false, + minValue: 0, + maxValue: 100, + thresholdLabels: true } }; @@ -270,6 +277,73 @@ class SingleStatCtrl extends MetricsPanelCtrl { return body; } + function addGauge() { + var plotCanvas = $('
'); + var plotCss = { + top: '10px', + margin: 'auto', + position: 'relative', + height: elem.height() + 'px', + width: elem.width() + 'px' + }; + + plotCanvas.css(plotCss); + + var thresholds = []; + for (var i = 0; i < data.thresholds.length; i++) { + thresholds.push({ + value: data.thresholds[i], + color: data.colorMap[i] + }); + } + thresholds.push({ + value: panel.gauge.maxValue, + color: data.colorMap[data.colorMap.length - 1] + }); + + var options = { + series: { + gauges: { + gauge: { + min: panel.gauge.minValue, + max: panel.gauge.maxValue, + background: { color: 'rgb(38,38,38)'}, + border: { color: null }, + shadow: { show: false }, + width: 38 + }, + frame: { show: false }, + label: { show: false }, + layout: { margin: 0 }, + cell: { border: { width: 0 } }, + threshold: { + values: thresholds, + label: { + show: panel.gauge.thresholdLabels, + margin: 8, + font: { size: 18 } + }, + width: 8 + }, + value: { + color: panel.colorValue ? getColorForValue(data, data.valueRounded) : null, + formatter: function () { return data.valueFormated; }, + font: { size: 30 } + }, + show: true + } + } + }; + + elem.append(plotCanvas); + + var plotSeries = { + data: [[0, data.valueRounded]] + }; + + $.plot(plotCanvas, [plotSeries], options); + } + function addSparkline() { var width = elem.width() + 20; if (width < 30) { @@ -335,7 +409,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { data = ctrl.data; setElementHeight(); - var body = getBigValueHtml(); + var body = panel.gauge.show ? '' : getBigValueHtml(); if (panel.colorBackground && !isNaN(data.valueRounded)) { var color = getColorForValue(data, data.valueRounded); @@ -358,6 +432,10 @@ class SingleStatCtrl extends MetricsPanelCtrl { addSparkline(); } + if (panel.gauge.show) { + addGauge(); + } + elem.toggleClass('pointer', panel.links.length > 0); if (panel.links.length > 0) { diff --git a/public/app/system.conf.js b/public/app/system.conf.js index 276988e5c34..40b70c0e30e 100644 --- a/public/app/system.conf.js +++ b/public/app/system.conf.js @@ -27,7 +27,8 @@ System.config({ "jquery.flot.stackpercent": "vendor/flot/jquery.flot.stackpercent", "jquery.flot.time": "vendor/flot/jquery.flot.time", "jquery.flot.crosshair": "vendor/flot/jquery.flot.crosshair", - "jquery.flot.fillbelow": "vendor/flot/jquery.flot.fillbelow" + "jquery.flot.fillbelow": "vendor/flot/jquery.flot.fillbelow", + "jquery.flot.gauge": "vendor/flot/jquery.flot.gauge" }, packages: { diff --git a/public/test/test-main.js b/public/test/test-main.js index d40955022fe..dbee086c250 100644 --- a/public/test/test-main.js +++ b/public/test/test-main.js @@ -35,7 +35,8 @@ "jquery.flot.stackpercent": "vendor/flot/jquery.flot.stackpercent", "jquery.flot.time": "vendor/flot/jquery.flot.time", "jquery.flot.crosshair": "vendor/flot/jquery.flot.crosshair", - "jquery.flot.fillbelow": "vendor/flot/jquery.flot.fillbelow" + "jquery.flot.fillbelow": "vendor/flot/jquery.flot.fillbelow", + "jquery.flot.gauge": "vendor/flot/jquery.flot.gauge" }, packages: { diff --git a/public/vendor/flot/jquery.flot.gauge.js b/public/vendor/flot/jquery.flot.gauge.js index 02aa553b7fa..d8ed958e990 100644 --- a/public/vendor/flot/jquery.flot.gauge.js +++ b/public/vendor/flot/jquery.flot.gauge.js @@ -277,7 +277,7 @@ */ Gauge.prototype.drawBackground = function(layout) { - if (!options.grid.show) { + if (!gaugeOptions.frame.show) { return; } context.save(); @@ -301,7 +301,7 @@ Gauge.prototype.drawCellBackground = function(gaugeOptionsi, cellLayout) { context.save(); - if (gaugeOptionsi.cell.border && gaugeOptionsi.cell.border.color && gaugeOptionsi.cell.border.width) { + if (gaugeOptionsi.cell.border && gaugeOptionsi.cell.border.show && gaugeOptionsi.cell.border.color && gaugeOptionsi.cell.border.width) { context.strokeStyle = gaugeOptionsi.cell.border.color; context.lineWidth = gaugeOptionsi.cell.border.width; context.strokeRect(cellLayout.x, cellLayout.y, cellLayout.cellWidth, cellLayout.cellHeight); @@ -337,9 +337,9 @@ layout.width, toRad(gaugeOptionsi.gauge.startAngle), toRad(gaugeOptionsi.gauge.endAngle), - gaugeOptionsi.gauge.stroke.color, // line color - gaugeOptionsi.gauge.stroke.width, // line width - gaugeOptionsi.gauge.frameColor, // fill color + gaugeOptionsi.gauge.border.color, // line color + gaugeOptionsi.gauge.border.width, // line width + gaugeOptionsi.gauge.background.color, // fill color blur); // draw gauge @@ -583,7 +583,7 @@ * @param {Number} [a] the angle of the value drawn */ function drawText(x, y, id, text, textOptions, a) { - var span = $("#" + id); + var span = $("." + id, placeholder); var exists = span.length; if (!exists) { span = $("") @@ -621,7 +621,6 @@ return Gauge; })(); - /** * get a instance of Logger * @@ -849,11 +848,15 @@ vMargin: 5, square: false }, + frame: { + show: true + }, cell: { background: { color: null }, border: { + show: true, color: "black", width: 1 }, @@ -866,15 +869,17 @@ endAngle: 2.1, // 0 - 2 factor of the radians min: 0, max: 100, - shadow: { - show: true, - blur: 5 + background: { + color: "white" }, - stroke: { + border: { color: "lightgray", width: 2 }, - frameColor: "white" + shadow: { + show: true, + blur: 5 + } }, label: { show: true, From 0c6841bdc77639d7a8bc25071269be648bb0a75c Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 19 Apr 2016 14:44:29 +0200 Subject: [PATCH 17/47] fix(singlestat): adds support for fontsizes in gagues --- public/app/plugins/panel/singlestat/module.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index 19909f08441..c8ebd0b0561 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -328,7 +328,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { value: { color: panel.colorValue ? getColorForValue(data, data.valueRounded) : null, formatter: function () { return data.valueFormated; }, - font: { size: 30 } + font: { size: getGaugeFontSize() } }, show: true } @@ -344,6 +344,15 @@ class SingleStatCtrl extends MetricsPanelCtrl { $.plot(plotCanvas, [plotSeries], options); } + function getGaugeFontSize() { + if (panel.valueFontSize) { + var num = parseInt(panel.valueFontSize.substring(0, panel.valueFontSize.length - 1)); + return 30 * (num / 100); + } else { + return 30; + } + } + function addSparkline() { var width = elem.width() + 20; if (width < 30) { @@ -405,7 +414,6 @@ class SingleStatCtrl extends MetricsPanelCtrl { function render() { if (!ctrl.data) { return; } - data = ctrl.data; setElementHeight(); From 0108b5eb883841b78d9b7986f941905065007dd9 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 19 Apr 2016 14:23:51 +0200 Subject: [PATCH 18/47] fix(singlestat): adds support for lighttheme --- public/app/plugins/panel/singlestat/module.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index c8ebd0b0561..6d60a70269e 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -7,6 +7,7 @@ import 'jquery.flot'; import 'jquery.flot.gauge'; import kbn from 'app/core/utils/kbn'; +import config from 'app/core/config'; import TimeSeries from 'app/core/time_series2'; import {MetricsPanelCtrl} from 'app/plugins/sdk'; @@ -283,7 +284,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { top: '10px', margin: 'auto', position: 'relative', - height: elem.height() + 'px', + height: (elem.height() * 0.9) + 'px', width: elem.width() + 'px' }; @@ -301,13 +302,17 @@ class SingleStatCtrl extends MetricsPanelCtrl { color: data.colorMap[data.colorMap.length - 1] }); + var bgColor = config.bootData.user.lightTheme + ? 'rgb(230,230,230)' + : 'rgb(38,38,38)'; + var options = { series: { gauges: { gauge: { min: panel.gauge.minValue, max: panel.gauge.maxValue, - background: { color: 'rgb(38,38,38)'}, + background: { color: bgColor }, border: { color: null }, shadow: { show: false }, width: 38 From 8d1ac8c7f585e8174b53d3d9afe9c3d3018d055c Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Wed, 20 Apr 2016 14:33:08 +0900 Subject: [PATCH 19/47] (cloudwatch) add kinesis metrics --- pkg/api/cloudwatch/metrics.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/api/cloudwatch/metrics.go b/pkg/api/cloudwatch/metrics.go index f5e5274a202..5ecdaabf516 100644 --- a/pkg/api/cloudwatch/metrics.go +++ b/pkg/api/cloudwatch/metrics.go @@ -56,7 +56,7 @@ func init() { "HbaseBackupFailed", "MostRecentBackupDuration", "TimeSinceLastSuccessfulBackup"}, "AWS/ES": {"ClusterStatus.green", "ClusterStatus.yellow", "ClusterStatus.red", "Nodes", "SearchableDocuments", "DeletedDocuments", "CPUUtilization", "FreeStorageSpace", "JVMMemoryPressure", "AutomatedSnapshotFailure", "MasterCPUUtilization", "MasterFreeStorageSpace", "MasterJVMMemoryPressure", "ReadLatency", "WriteLatency", "ReadThroughput", "WriteThroughput", "DiskQueueLength", "ReadIOPS", "WriteIOPS"}, "AWS/Events": {"Invocations", "FailedInvocations", "TriggeredRules", "MatchedEvents", "ThrottledRules"}, - "AWS/Kinesis": {"PutRecord.Bytes", "PutRecord.Latency", "PutRecord.Success", "PutRecords.Bytes", "PutRecords.Latency", "PutRecords.Records", "PutRecords.Success", "IncomingBytes", "IncomingRecords", "GetRecords.Bytes", "GetRecords.IteratorAgeMilliseconds", "GetRecords.Latency", "GetRecords.Success"}, + "AWS/Kinesis": {"GetRecords.Bytes", "GetRecords.IteratorAge", "GetRecords.IteratorAgeMilliseconds", "GetRecords.Latency", "GetRecords.Records", "GetRecords.Success", "IncomingBytes", "IncomingRecords", "PutRecord.Bytes", "PutRecord.Latency", "PutRecord.Success", "PutRecords.Bytes", "PutRecords.Latency", "PutRecords.Records", "PutRecords.Success", "ReadProvisionedThroughputExceeded", "WriteProvisionedThroughputExceeded", "IteratorAgeMilliseconds", "OutgoingBytes", "OutgoingRecords"}, "AWS/Lambda": {"Invocations", "Errors", "Duration", "Throttles"}, "AWS/Logs": {"IncomingBytes", "IncomingLogEvents", "ForwardedBytes", "ForwardedLogEvents", "DeliveryErrors", "DeliveryThrottling"}, "AWS/ML": {"PredictCount", "PredictFailureCount"}, @@ -88,7 +88,7 @@ func init() { "AWS/ElasticMapReduce": {"ClusterId", "JobFlowId", "JobId"}, "AWS/ES": {}, "AWS/Events": {"RuleName"}, - "AWS/Kinesis": {"StreamName"}, + "AWS/Kinesis": {"StreamName", "ShardID"}, "AWS/Lambda": {"FunctionName"}, "AWS/Logs": {"LogGroupName", "DestinationType", "FilterName"}, "AWS/ML": {"MLModelId", "RequestMode"}, From 3925bcb94cae91181259907fcc65c95062baaf33 Mon Sep 17 00:00:00 2001 From: Shaunak Kashyap Date: Wed, 20 Apr 2016 08:47:16 -0700 Subject: [PATCH 20/47] Updating Elasticsearch logo --- .../elasticsearch/img/logo_large.png | Bin 40972 -> 38427 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/public/app/plugins/datasource/elasticsearch/img/logo_large.png b/public/app/plugins/datasource/elasticsearch/img/logo_large.png index 429dedac1a7c94faa89a84d907e93c637975a18a..b76d01e5d74cf26415174edce6852366edf7c42e 100644 GIT binary patch literal 38427 zcmdSB1y>zGlrD-K+}+`DaQEPD2X}|y7TkgdcY=EeF2SAP!QI{6U4z3TGjr$Och|Z< zV0x`yUA_COy{oEscUSHDsw0#YrBM(G5g{NTP-MUost^#6asPIB*bj;SpHHJ72Y{uh zf+z$;O)S!jG0ev~&=jny00H4e{ecLEfOz_cJcNL7V}*b?HiCfQOM`&GbIfc}5%{=( z;0V@nfq+25`nN+uWMtxhXrHrI*LKxbkmoaXuwynhb1*Sy_ONsOKtn(Xc<_BB?aW<` zfgX0Y_AY!Lf}sCE@O`BJiCI9v|A4sK2!gZ~l!4+7&gMWaW=>{SkPspe2o!KOv*1&e zkovFeA7_FfD_2)XJ{A^tcXwuY4rT{uOBOa>US1Yfb{2MarVj*@i>JM-u?LgA3&npb z`Cob@%w0^KtsPyh9qfVs^ctHuxVZ{~K>rN=_x7LVbhWnl-1&uyb@acX9d9FT^JBAISfo?0?(&PrQ<|wfRR$|CQ&z@&Bjn|Hdm@ zIk-A}w3@TEsf@j=x$}py|H{q&U)KKLi2u?h!1Ax+|3^doXDk1Q^rM-D5CvHN`<4kI zLM{4#c!&sujD)DV2jr{uoAp|6zyWNPk9~^Gsd}Whg)ry6R@^ zV)RZ0&)1#B2vcNfL?&1LG9~xZ#oSmMtt0N+$3tQMuTK{h1}A?g0P6&U{juAN-?{mm zRzLE*`5vVjxvKh%!Lvd|Lr^H&1H!?-n2mqgb*sy!2?};kAGj)+BS@wr$~?lE&+Lgk z*6uNSFAW0_EcGbhS>aSEP;`{$CIJCW5pp3l(5uk+kZ5=Jh@z+W01)Qp4CDGR5kV=E1 zaS=iTTu%bhw}|{)MySmZ=~{pP!d)EjAGf&XlQIO?q%@`ds*K`OGq^77yT=burRW3{ z%!a#sOB90$YQQ#lsK90aq&G+_A%U;I<`Y5dND_3^%%MM`Zw9Vej1P>Fn;gP3&B+R@ zx7cbYXtEz*i*_xK3lUD~WO=4TPK@&5-E9tAAeKgeqZ-~kZ0@Flf>xcegr7s|c9xgD zV+Z#@mh>VG-oT(|L#McYUiEWTyvX>jRD4rFA1*$1Cm6|{-G!FNN=^S5x9z5kt1yMNYway(skjS(O7ZYxz5A6cH1r5zIB#sz!k!q&?S7benc zB1Oq4zNOsgy_3_K&x60*Wetxfjm9!5$am;>eJdbk#;Xpyq|5-G>Zyj;dLc68u! z>aIRB^JGiE*@11gqE33smH+{(p6O~ZABt)HX~%sHjGWnW z6k(_nj4NiU(SCaqTY@+%aT`K%2(v*}j|8`6p}(Q=Un)IiRpa{SgKwl(%^jr9`3y z14oFd9wNvl4CzsJ+(k9kp^h(M9SKNqdq2@3hbl8a1A_Lq3vtC&#*^Q<9snUth@cT5 zt+Af$VL&h(qClL#$>;C^0!Yf@nvh?ybiBu`mq

Omjk$)=+h|7g9Z3&w#>yDrX?G*GS=KdS^II7sD$ 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 P`fr-5AeBlb>#+X;Yv(*z literal 40972 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 From 7079b5d770eddfa5207973b16d74cd7a5fc29f7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 20 Apr 2016 09:15:45 -0700 Subject: [PATCH 21/47] fix(dashboard list): fixed minor issue with dashboard list panel, fixes #4768 --- CHANGELOG.md | 1 + public/app/plugins/panel/dashlist/module.ts | 23 ++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c57bfcfee2..36f644196fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * **InfluxDB 0.12**: Fixed issue templating and `show tag values` query only returning tags for first measurement, fixes [#4726](https://github.com/grafana/grafana/issues/4726) * **Templating**: Fixed issue with regex formating when matching multiple values, fixes [#4755](https://github.com/grafana/grafana/issues/4755) * **Templating**: Fixed issue with custom all value and escaping, fixes [#4736](https://github.com/grafana/grafana/issues/4736) +* **Dashlist**: Fixed issue dashboard list panel and caching tags, fixes [#4768](https://github.com/grafana/grafana/issues/4768) # 3.0.0-beta5 (2016-04-15) diff --git a/public/app/plugins/panel/dashlist/module.ts b/public/app/plugins/panel/dashlist/module.ts index 77029bd7ceb..b6407cf103e 100644 --- a/public/app/plugins/panel/dashlist/module.ts +++ b/public/app/plugins/panel/dashlist/module.ts @@ -5,27 +5,26 @@ import config from 'app/core/config'; import {PanelCtrl} from 'app/plugins/sdk'; import {impressions} from 'app/features/dashboard/impression_store'; - // Set and populate defaults -var panelDefaults = { - query: '', - limit: 10, - tags: [], - recent: false, - search: false, - starred: true, - headings: true, -}; - class DashListCtrl extends PanelCtrl { static templateUrl = 'module.html'; groups: any[]; modes: any[]; + panelDefaults = { + query: '', + limit: 10, + tags: [], + recent: false, + search: false, + starred: true, + headings: true, + }; + /** @ngInject */ constructor($scope, $injector, private backendSrv) { super($scope, $injector); - _.defaults(this.panel, panelDefaults); + _.defaults(this.panel, this.panelDefaults); if (this.panel.tag) { this.panel.tags = [this.panel.tag]; From 51de894692ed38d9e5917077397ab2c110555ce1 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 21 Apr 2016 09:59:48 +0200 Subject: [PATCH 22/47] fix(snapshots): sets default value for snapshot name make it possible for < 3.0 instances to publish snapshots ref #4778 --- pkg/api/dashboard_snapshot.go | 4 ++++ pkg/models/dashboard_snapshot.go | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/api/dashboard_snapshot.go b/pkg/api/dashboard_snapshot.go index 3c369ca5b7f..cdcb871314d 100644 --- a/pkg/api/dashboard_snapshot.go +++ b/pkg/api/dashboard_snapshot.go @@ -21,6 +21,10 @@ func GetSharingOptions(c *middleware.Context) { } func CreateDashboardSnapshot(c *middleware.Context, cmd m.CreateDashboardSnapshotCommand) { + if cmd.Name == "" { + cmd.Name = "Unnamed snapshot" + } + if cmd.External { // external snapshot ref requires key and delete key if cmd.Key == "" || cmd.DeleteKey == "" { diff --git a/pkg/models/dashboard_snapshot.go b/pkg/models/dashboard_snapshot.go index f920e91f2e4..4a4d1290b6d 100644 --- a/pkg/models/dashboard_snapshot.go +++ b/pkg/models/dashboard_snapshot.go @@ -45,7 +45,7 @@ type DashboardSnapshotDTO struct { type CreateDashboardSnapshotCommand struct { Dashboard *simplejson.Json `json:"dashboard" binding:"Required"` - Name string `json:"name" binding:"Required"` + Name string `json:"name"` Expires int64 `json:"expires"` // these are passed when storing an external snapshot ref From 0fb4141ce4efe2b603b374d64d2b7514d0d34726 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 21 Apr 2016 10:23:09 +0200 Subject: [PATCH 23/47] docs(changelog): add note about scrollbar fix #4769 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36f644196fc..d5b7dbf495c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * **Templating**: Fixed issue with regex formating when matching multiple values, fixes [#4755](https://github.com/grafana/grafana/issues/4755) * **Templating**: Fixed issue with custom all value and escaping, fixes [#4736](https://github.com/grafana/grafana/issues/4736) * **Dashlist**: Fixed issue dashboard list panel and caching tags, fixes [#4768](https://github.com/grafana/grafana/issues/4768) +* **Graph**: Fixed issue with unneeded scrollbar in legend for Firefox, fixes [#4760](https://github.com/grafana/grafana/issues/4760) # 3.0.0-beta5 (2016-04-15) From 297c829bdc3247d1dbdc7e1c441f31059ed3f3df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 22 Apr 2016 09:47:30 +0200 Subject: [PATCH 24/47] tech(): updatad sass-lint --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f454a42ba7a..e71a9049dde 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "phantomjs-prebuilt": "^2.1.3", "reflect-metadata": "0.1.2", "rxjs": "5.0.0-beta.4", - "sass-lint": "^1.5.0", + "sass-lint": "^1.6.0", "systemjs": "0.19.24" }, "engines": { From dfe36fb702cf730d6aff344c4bfbb4e18a83a124 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 22 Apr 2016 11:41:56 +0200 Subject: [PATCH 25/47] feat(panel): added new panel event panel-initialized --- public/app/features/dashboard/viewStateSrv.js | 2 +- public/app/features/panel/panel_ctrl.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/public/app/features/dashboard/viewStateSrv.js b/public/app/features/dashboard/viewStateSrv.js index 2738105553a..cda5e69f006 100644 --- a/public/app/features/dashboard/viewStateSrv.js +++ b/public/app/features/dashboard/viewStateSrv.js @@ -36,7 +36,7 @@ function (angular, _, $) { self.update(payload); }); - $scope.onAppEvent('panel-instantiated', function(evt, payload) { + $scope.onAppEvent('panel-initialized', function(evt, payload) { self.registerPanel(payload.scope); }); diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index 4f3df7e6e8a..48746e56ff4 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -50,8 +50,11 @@ export class PanelCtrl { } init() { - this.publishAppEvent('panel-instantiated', {scope: this.$scope}); this.calculatePanelHeight(); + + this.publishAppEvent('panel-initialized', {scope: this.$scope}); + this.events.emit('panel-initialized'); + this.refresh(); } From de52ca47855658f8f41402ce76443c0d41738303 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 22 Apr 2016 11:54:09 +0200 Subject: [PATCH 26/47] docs(changelog): adds gauges to changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5b7dbf495c..90217553c19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # 3.0.0-beta6 (unreleased) +### Enhancements +* **Singlestat**: Support for gauges in singlestat panel. closes [#3688](https://github.com/grafana/grafana/pull/3688) + ### Bug fixes * **InfluxDB 0.12**: Fixed issue templating and `show tag values` query only returning tags for first measurement, fixes [#4726](https://github.com/grafana/grafana/issues/4726) * **Templating**: Fixed issue with regex formating when matching multiple values, fixes [#4755](https://github.com/grafana/grafana/issues/4755) From db15bf23b83fb3cee2fe5e811c32642099ea4e67 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 22 Apr 2016 12:09:02 +0200 Subject: [PATCH 27/47] fix(sass): fixes sass lint issue --- public/sass/components/_dropdown.scss | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/public/sass/components/_dropdown.scss b/public/sass/components/_dropdown.scss index 339284eb471..c4fbb20f1f3 100644 --- a/public/sass/components/_dropdown.scss +++ b/public/sass/components/_dropdown.scss @@ -67,18 +67,20 @@ } // Links within the dropdown menu - > li > a { - display: block; - padding: 3px 20px 3px 15px; - clear: both; - font-weight: normal; - line-height: $line-height-base; - color: $dropdownLinkColor; - white-space: nowrap; + > li { + > a { + display: block; + padding: 3px 20px 3px 15px; + clear: both; + font-weight: normal; + line-height: $line-height-base; + color: $dropdownLinkColor; + white-space: nowrap; - i { - padding-right: 5px; - color: $link-color-disabled; + i { + padding-right: 5px; + color: $link-color-disabled; + } } } } From 24fbcd8dea5e80cb3678579ecf5e38008a9b4ac9 Mon Sep 17 00:00:00 2001 From: Carl Bergquist Date: Fri, 22 Apr 2016 13:05:43 +0200 Subject: [PATCH 28/47] Revert "Gauges" --- .../app/plugins/panel/singlestat/editor.html | 37 - public/app/plugins/panel/singlestat/module.ts | 95 +- public/app/system.conf.js | 3 +- public/test/test-main.js | 3 +- public/vendor/flot/jquery.flot.gauge.js | 960 ------------------ 5 files changed, 4 insertions(+), 1094 deletions(-) delete mode 100644 public/vendor/flot/jquery.flot.gauge.js diff --git a/public/app/plugins/panel/singlestat/editor.html b/public/app/plugins/panel/singlestat/editor.html index 437445a2400..6b0806133b8 100644 --- a/public/app/plugins/panel/singlestat/editor.html +++ b/public/app/plugins/panel/singlestat/editor.html @@ -156,43 +156,6 @@

-
-
-
-
    -
  • - Gauge -
  • -
  • - Show  - - -
  • -
  • - Threshold labels  - - -
  • -
  • - Min -
  • -
  • - -
  • -
  • - Max -
  • -
  • - -
  • -
-
-
-
-
-
diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index 6d60a70269e..8af684c9d68 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -4,10 +4,8 @@ import angular from 'angular'; import _ from 'lodash'; import $ from 'jquery'; import 'jquery.flot'; -import 'jquery.flot.gauge'; import kbn from 'app/core/utils/kbn'; -import config from 'app/core/config'; import TimeSeries from 'app/core/time_series2'; import {MetricsPanelCtrl} from 'app/plugins/sdk'; @@ -40,12 +38,6 @@ var panelDefaults = { full: false, lineColor: 'rgb(31, 120, 193)', fillColor: 'rgba(31, 118, 189, 0.18)', - }, - gauge: { - show: false, - minValue: 0, - maxValue: 100, - thresholdLabels: true } }; @@ -278,86 +270,6 @@ class SingleStatCtrl extends MetricsPanelCtrl { return body; } - function addGauge() { - var plotCanvas = $('
'); - var plotCss = { - top: '10px', - margin: 'auto', - position: 'relative', - height: (elem.height() * 0.9) + 'px', - width: elem.width() + 'px' - }; - - plotCanvas.css(plotCss); - - var thresholds = []; - for (var i = 0; i < data.thresholds.length; i++) { - thresholds.push({ - value: data.thresholds[i], - color: data.colorMap[i] - }); - } - thresholds.push({ - value: panel.gauge.maxValue, - color: data.colorMap[data.colorMap.length - 1] - }); - - var bgColor = config.bootData.user.lightTheme - ? 'rgb(230,230,230)' - : 'rgb(38,38,38)'; - - var options = { - series: { - gauges: { - gauge: { - min: panel.gauge.minValue, - max: panel.gauge.maxValue, - background: { color: bgColor }, - border: { color: null }, - shadow: { show: false }, - width: 38 - }, - frame: { show: false }, - label: { show: false }, - layout: { margin: 0 }, - cell: { border: { width: 0 } }, - threshold: { - values: thresholds, - label: { - show: panel.gauge.thresholdLabels, - margin: 8, - font: { size: 18 } - }, - width: 8 - }, - value: { - color: panel.colorValue ? getColorForValue(data, data.valueRounded) : null, - formatter: function () { return data.valueFormated; }, - font: { size: getGaugeFontSize() } - }, - show: true - } - } - }; - - elem.append(plotCanvas); - - var plotSeries = { - data: [[0, data.valueRounded]] - }; - - $.plot(plotCanvas, [plotSeries], options); - } - - function getGaugeFontSize() { - if (panel.valueFontSize) { - var num = parseInt(panel.valueFontSize.substring(0, panel.valueFontSize.length - 1)); - return 30 * (num / 100); - } else { - return 30; - } - } - function addSparkline() { var width = elem.width() + 20; if (width < 30) { @@ -419,10 +331,11 @@ class SingleStatCtrl extends MetricsPanelCtrl { function render() { if (!ctrl.data) { return; } + data = ctrl.data; setElementHeight(); - var body = panel.gauge.show ? '' : getBigValueHtml(); + var body = getBigValueHtml(); if (panel.colorBackground && !isNaN(data.valueRounded)) { var color = getColorForValue(data, data.valueRounded); @@ -445,10 +358,6 @@ class SingleStatCtrl extends MetricsPanelCtrl { addSparkline(); } - if (panel.gauge.show) { - addGauge(); - } - elem.toggleClass('pointer', panel.links.length > 0); if (panel.links.length > 0) { diff --git a/public/app/system.conf.js b/public/app/system.conf.js index 40b70c0e30e..276988e5c34 100644 --- a/public/app/system.conf.js +++ b/public/app/system.conf.js @@ -27,8 +27,7 @@ System.config({ "jquery.flot.stackpercent": "vendor/flot/jquery.flot.stackpercent", "jquery.flot.time": "vendor/flot/jquery.flot.time", "jquery.flot.crosshair": "vendor/flot/jquery.flot.crosshair", - "jquery.flot.fillbelow": "vendor/flot/jquery.flot.fillbelow", - "jquery.flot.gauge": "vendor/flot/jquery.flot.gauge" + "jquery.flot.fillbelow": "vendor/flot/jquery.flot.fillbelow" }, packages: { diff --git a/public/test/test-main.js b/public/test/test-main.js index dbee086c250..d40955022fe 100644 --- a/public/test/test-main.js +++ b/public/test/test-main.js @@ -35,8 +35,7 @@ "jquery.flot.stackpercent": "vendor/flot/jquery.flot.stackpercent", "jquery.flot.time": "vendor/flot/jquery.flot.time", "jquery.flot.crosshair": "vendor/flot/jquery.flot.crosshair", - "jquery.flot.fillbelow": "vendor/flot/jquery.flot.fillbelow", - "jquery.flot.gauge": "vendor/flot/jquery.flot.gauge" + "jquery.flot.fillbelow": "vendor/flot/jquery.flot.fillbelow" }, packages: { diff --git a/public/vendor/flot/jquery.flot.gauge.js b/public/vendor/flot/jquery.flot.gauge.js deleted file mode 100644 index d8ed958e990..00000000000 --- a/public/vendor/flot/jquery.flot.gauge.js +++ /dev/null @@ -1,960 +0,0 @@ -/*! - * jquery.flot.gauge v1.1.0 * - * - * Flot plugin for rendering gauge charts. - * - * Copyright (c) 2015 @toyoty99. - * Licensed under the MIT license. - */ - -/** - * @module flot.gauge - */ -(function($) { - - - /** - * Gauge class - * - * @class Gauge - */ - var Gauge = (function() { - /** - * context of canvas - * - * @property context - * @type Object - */ - var context; - /** - * placeholder of canvas - * - * @property placeholder - * @type Object - */ - var placeholder; - /** - * options of plot - * - * @property options - * @type Object - */ - var options; - /** - * options of gauge - * - * @property gaugeOptions - * @type Object - */ - var gaugeOptions; - /** - * data series - * - * @property series - * @type Array - */ - var series; - /** - * logger - * - * @property logger - * @type Object - */ - var logger; - - /** - * constructor - * - * @class Gauge - * @constructor - * @param {Object} gaugeOptions gauge options - */ - var Gauge = function(plot, ctx) { - context = ctx; - placeholder = plot.getPlaceholder(); - options = plot.getOptions(); - gaugeOptions = options.series.gauges; - series = plot.getData(); - logger = getLogger(gaugeOptions.debug); - } - - /** - * calculate layout - * - * @method calculateLayout - * @return the calculated layout properties - */ - Gauge.prototype.calculateLayout = function() { - - var canvasWidth = placeholder.width(); - var canvasHeight = placeholder.height(); - - - - // calculate cell size - var columns = Math.min(series.length, gaugeOptions.layout.columns); - var rows = Math.ceil(series.length / columns); - - - - var margin = gaugeOptions.layout.margin; - var hMargin = gaugeOptions.layout.hMargin; - var vMargin = gaugeOptions.layout.vMargin; - var cellWidth = (canvasWidth - (margin * 2) - (hMargin * (columns - 1))) / columns; - var cellHeight = (canvasHeight - (margin * 2) - (vMargin * (rows - 1))) / rows; - if (gaugeOptions.layout.square) { - var cell = Math.min(cellWidth, cellHeight); - cellWidth = cell; - cellHeight = cell; - } - - - - // calculate 'auto' values - calculateAutoValues(gaugeOptions, cellWidth); - - // calculate maximum radius - var cellMargin = gaugeOptions.cell.margin; - var labelMargin = 0; - var labelFontSize = 0; - if (gaugeOptions.label.show) { - labelMargin = gaugeOptions.label.margin; - labelFontSize = gaugeOptions.label.font.size; - } - var valueMargin = 0; - var valueFontSize = 0; - if (gaugeOptions.value.show) { - valueMargin = gaugeOptions.value.margin; - valueFontSize = gaugeOptions.value.font.size; - } - var thresholdWidth = 0; - if (gaugeOptions.threshold.show) { - thresholdWidth = gaugeOptions.threshold.width; - } - var thresholdLabelMargin = 0; - var thresholdLabelFontSize = 0; - if (gaugeOptions.threshold.label.show) { - thresholdLabelMargin = gaugeOptions.threshold.label.margin; - thresholdLabelFontSize = gaugeOptions.threshold.label.font.size; - } - - var maxRadiusH = (cellWidth / 2) - cellMargin - thresholdWidth - (thresholdLabelMargin * 2) - thresholdLabelFontSize; - - var startAngle = gaugeOptions.gauge.startAngle; - var endAngle = gaugeOptions.gauge.endAngle; - var dAngle = (endAngle - startAngle) / 100; - var heightRatioV = -1; - for (var a = startAngle; a < endAngle; a += dAngle) { - heightRatioV = Math.max(heightRatioV, Math.sin(toRad(a))); - } - heightRatioV = Math.max(heightRatioV, Math.sin(toRad(endAngle))); - var outerRadiusV = (cellHeight - (cellMargin * 2) - (labelMargin * 2) - labelFontSize) / (1 + heightRatioV); - if (outerRadiusV * heightRatioV < valueMargin + (valueFontSize / 2)) { - outerRadiusV = cellHeight - (cellMargin * 2) - (labelMargin * 2) - labelFontSize - valueMargin - (valueFontSize / 2); - } - var maxRadiusV = outerRadiusV - (thresholdLabelMargin * 2) - thresholdLabelFontSize - thresholdWidth; - - var radius = Math.min(maxRadiusH, maxRadiusV); - - - var width = gaugeOptions.gauge.width; - if (width >= radius) { - width = Math.max(3, radius / 3); - } - - - var outerRadius = (thresholdLabelMargin * 2) + thresholdLabelFontSize + thresholdWidth + radius; - var gaugeOuterHeight = Math.max(outerRadius * (1 + heightRatioV), outerRadius + valueMargin + (valueFontSize / 2)); - - return { - canvasWidth: canvasWidth, - canvasHeight: canvasHeight, - margin: margin, - hMargin: hMargin, - vMargin: vMargin, - columns: columns, - rows: rows, - cellWidth: cellWidth, - cellHeight: cellHeight, - cellMargin: cellMargin, - labelMargin: labelMargin, - labelFontSize: labelFontSize, - valueMargin: valueMargin, - valueFontSize: valueFontSize, - width: width, - radius: radius, - thresholdWidth: thresholdWidth, - thresholdLabelMargin: thresholdLabelMargin, - thresholdLabelFontSize: thresholdLabelFontSize, - gaugeOuterHeight: gaugeOuterHeight - }; - } - - /** - * calculate the values which are set as 'auto' - * - * @method calculateAutoValues - * @param {Object} gaugeOptionsi the options of the gauge - * @param {Number} cellWidth the width of cell - */ - function calculateAutoValues(gaugeOptionsi, cellWidth) { - - if (gaugeOptionsi.gauge.width === "auto") { - gaugeOptionsi.gauge.width = Math.max(5, cellWidth / 8); - } - if (gaugeOptionsi.label.margin === "auto") { - gaugeOptionsi.label.margin = Math.max(1, cellWidth / 20); - } - if (gaugeOptionsi.label.font.size === "auto") { - gaugeOptionsi.label.font.size = Math.max(5, cellWidth / 8); - } - if (gaugeOptionsi.value.margin === "auto") { - gaugeOptionsi.value.margin = Math.max(1, cellWidth / 30); - } - if (gaugeOptionsi.value.font.size === "auto") { - gaugeOptionsi.value.font.size = Math.max(5, cellWidth / 9); - } - if (gaugeOptionsi.threshold.width === "auto") { - gaugeOptionsi.threshold.width = Math.max(3, cellWidth / 100); - } - if (gaugeOptionsi.threshold.label.margin === "auto") { - gaugeOptionsi.threshold.label.margin = Math.max(3, cellWidth / 40); - } - if (gaugeOptionsi.threshold.label.font.size === "auto") { - gaugeOptionsi.threshold.label.font.size = Math.max(5, cellWidth / 15); - } - - } - Gauge.prototype.calculateAutoValues = calculateAutoValues; - - /** - * calculate the layout of the cell inside - * - * @method calculateCellLayout - * @param {Object} gaugeOptionsi the options of the gauge - * @param {Number} cellWidth the width of cell - * @param {Number} i the index of the series - * @return the calculated cell layout properties - */ - Gauge.prototype.calculateCellLayout = function(gaugeOptionsi, layout, i) { - - // calculate top, left and center - var c = col(layout.columns, i); - var r = row(layout.columns, i); - var x = layout.margin + (layout.cellWidth + layout.hMargin) * c; - var y = layout.margin + (layout.cellHeight + layout.vMargin) * r; - var cx = x + (layout.cellWidth / 2); - var cy = y + layout.cellMargin + (layout.labelMargin * 2) + layout.labelFontSize + layout.thresholdWidth - + layout.thresholdLabelFontSize + (layout.thresholdLabelMargin * 2) + layout.radius; - var blank = layout.cellHeight - (layout.cellMargin * 2) - (layout.labelMargin * 2) - layout.labelFontSize - layout.gaugeOuterHeight; - var offsetY = 0; - if (gaugeOptionsi.cell.vAlign === "middle") { - offsetY = (blank / 2); - } else if (gaugeOptionsi.cell.vAlign === "bottom") { - offsetY = blank; - } - cy += offsetY; - - return { - col: c, - row: r, - x: x, - y: y, - offsetY: offsetY, - cellWidth: layout.cellWidth, - cellHeight: layout.cellHeight, - cellMargin: layout.cellMargin, - cx: cx, - cy: cy - } - } - - /** - * draw the background of chart - * - * @method drawBackground - * @param {Object} layout the layout properties - */ - Gauge.prototype.drawBackground = function(layout) { - - if (!gaugeOptions.frame.show) { - return; - } - context.save(); - context.strokeStyle = options.grid.borderColor; - context.lineWidth = options.grid.borderWidth; - context.strokeRect(0, 0, layout.canvasWidth, layout.canvasHeight); - if (options.grid.backgroundColor) { - context.fillStyle = options.grid.backgroundColor; - context.fillRect(0, 0, layout.canvasWidth, layout.canvasHeight); - } - context.restore(); - } - - /** - * draw the background of cell - * - * @method drawCellBackground - * @param {Object} gaugeOptionsi the options of the gauge - * @param {Object} cellLayout the cell layout properties - */ - Gauge.prototype.drawCellBackground = function(gaugeOptionsi, cellLayout) { - - context.save(); - if (gaugeOptionsi.cell.border && gaugeOptionsi.cell.border.show && gaugeOptionsi.cell.border.color && gaugeOptionsi.cell.border.width) { - context.strokeStyle = gaugeOptionsi.cell.border.color; - context.lineWidth = gaugeOptionsi.cell.border.width; - context.strokeRect(cellLayout.x, cellLayout.y, cellLayout.cellWidth, cellLayout.cellHeight); - } - if (gaugeOptionsi.cell.background && gaugeOptionsi.cell.background.color) { - context.fillStyle = gaugeOptionsi.cell.background.color; - context.fillRect(cellLayout.x, cellLayout.y, cellLayout.cellWidth, cellLayout.cellHeight); - } - context.restore(); - } - - /** - * draw the gauge - * - * @method drawGauge - * @param {Object} gaugeOptionsi the options of the gauge - * @param {Object} layout the layout properties - * @param {Object} cellLayout the cell layout properties - * @param {String} label the label of data - * @param {Number} data the value of the gauge - */ - Gauge.prototype.drawGauge = function(gaugeOptionsi, layout, cellLayout, label, data) { - - - var blur = gaugeOptionsi.gauge.shadow.show ? gaugeOptionsi.gauge.shadow.blur : 0; - - - // draw gauge frame - drawArcWithShadow( - cellLayout.cx, // center x - cellLayout.cy, // center y - layout.radius, - layout.width, - toRad(gaugeOptionsi.gauge.startAngle), - toRad(gaugeOptionsi.gauge.endAngle), - gaugeOptionsi.gauge.border.color, // line color - gaugeOptionsi.gauge.border.width, // line width - gaugeOptionsi.gauge.background.color, // fill color - blur); - - // draw gauge - var c1 = getColor(gaugeOptionsi, data); - var a2 = calculateAngle(gaugeOptionsi, layout, data); - drawArcWithShadow( - cellLayout.cx, // center x - cellLayout.cy, // center y - layout.radius - 1, - layout.width - 2, - toRad(gaugeOptionsi.gauge.startAngle), - toRad(a2), - c1, // line color - 1, // line width - c1, // fill color - blur); - } - - /** - * decide the color of the data from the threshold options - * - * @method getColor - * @private - * @param {Object} gaugeOptionsi the options of the gauge - * @param {Number} data the value of the gauge - */ - function getColor(gaugeOptionsi, data) { - var color; - for (var i = 0; i < gaugeOptionsi.threshold.values.length; i++) { - var threshold = gaugeOptionsi.threshold.values[i]; - color = threshold.color; - if (data <= threshold.value) { - break; - } - } - return color; - } - - /** - * calculate the angle of the data - * - * @method calculateAngle - * @private - * @param {Object} gaugeOptionsi the options of the gauge - * @param {Object} layout the layout properties - * @param {Number} data the value of the gauge - */ - function calculateAngle(gaugeOptionsi, layout, data) { - var a = - gaugeOptionsi.gauge.startAngle - + (gaugeOptionsi.gauge.endAngle - gaugeOptionsi.gauge.startAngle) - * ((data - gaugeOptionsi.gauge.min) / (gaugeOptionsi.gauge.max - gaugeOptionsi.gauge.min)); - - if (a < gaugeOptionsi.gauge.startAngle) { - a = gaugeOptionsi.gauge.startAngle; - } else if (a > gaugeOptionsi.gauge.endAngle) { - a = gaugeOptionsi.gauge.endAngle; - } - return a; - } - - /** - * draw the arc of the threshold - * - * @method drawThreshold - * @param {Object} gaugeOptionsi the options of the gauge - * @param {Object} layout the layout properties - * @param {Object} cellLayout the cell layout properties - */ - Gauge.prototype.drawThreshold = function(gaugeOptionsi, layout, cellLayout) { - - var a1 = gaugeOptionsi.gauge.startAngle; - for (var i = 0; i < gaugeOptionsi.threshold.values.length; i++) { - var threshold = gaugeOptionsi.threshold.values[i]; - c1 = threshold.color; - a2 = calculateAngle(gaugeOptionsi, layout, threshold.value); - drawArc( - context, - cellLayout.cx, // center x - cellLayout.cy, // center y - layout.radius + layout.thresholdWidth, - layout.thresholdWidth - 2, - toRad(a1), - toRad(a2), - c1, // line color - 1, // line width - c1); // fill color - a1 = a2; - } - } - - /** - * draw an arc with a shadow - * - * @method drawArcWithShadow - * @private - * @param {Number} cx the x position of the center - * @param {Number} cy the y position of the center - * @param {Number} r the radius of an arc - * @param {Number} w the width of an arc - * @param {Number} rd1 the start angle of an arc in radians - * @param {Number} rd2 the end angle of an arc in radians - * @param {String} lc the color of a line - * @param {Number} lw the widht of a line - * @param {String} fc the fill color of an arc - * @param {Number} blur the shdow blur - */ - function drawArcWithShadow(cx, cy, r, w, rd1, rd2, lc, lw, fc, blur) { - if (rd1 === rd2) { - return; - } - context.save(); - - drawArc(context, cx, cy, r, w, rd1, rd2, lc, lw, fc); - - if (blur) { - drawArc(context, cx, cy, r, w, rd1, rd2); - context.clip(); - context.shadowOffsetX = 0; - context.shadowOffsetY = 0; - context.shadowBlur = 10; - context.shadowColor = "gray"; - drawArc(context, cx, cy, r + 1, w + 2, rd1, rd2, lc, 1); - } - context.restore(); - } - - /** - * draw the label of the gauge - * - * @method drawLable - * @param {Object} gaugeOptionsi the options of the gauge - * @param {Object} layout the layout properties - * @param {Object} cellLayout the cell layout properties - * @param {Number} i the index of the series - * @param {Object} item the item of the series - */ - Gauge.prototype.drawLable = function(gaugeOptionsi, layout, cellLayout, i, item) { - - drawText( - cellLayout.cx, - cellLayout.y + cellLayout.cellMargin + layout.labelMargin + cellLayout.offsetY, - "flotGagueLabel" + i, - gaugeOptionsi.label.formatter ? gaugeOptionsi.label.formatter(item.label, item.data[0][1]) : text, - gaugeOptionsi.label); - } - - /** - * draw the value of the gauge - * - * @method drawValue - * @param {Object} gaugeOptionsi the options of the gauge - * @param {Object} layout the layout properties - * @param {Object} cellLayout the cell layout properties - * @param {Number} i the index of the series - * @param {Object} item the item of the series - */ - Gauge.prototype.drawValue = function(gaugeOptionsi, layout, cellLayout, i, item) { - - drawText( - cellLayout.cx, - cellLayout.cy - (gaugeOptionsi.value.font.size / 2), - "flotGagueValue" + i, - gaugeOptionsi.value.formatter ? gaugeOptionsi.value.formatter(item.label, item.data[0][1]) : text, - gaugeOptionsi.value); - } - - /** - * draw the values of the threshold - * - * @method drawThresholdValues - * @param {Object} gaugeOptionsi the options of the gauge - * @param {Object} layout the layout properties - * @param {Object} cellLayout the cell layout properties - * @param {Number} i the index of the series - */ - Gauge.prototype.drawThresholdValues = function(gaugeOptionsi, layout, cellLayout, i) { - - // min, max - drawThresholdValue(gaugeOptionsi, layout, cellLayout, "Min" + i, gaugeOptionsi.gauge.min, gaugeOptionsi.gauge.startAngle); - drawThresholdValue(gaugeOptionsi, layout, cellLayout, "Max" + i, gaugeOptionsi.gauge.max, gaugeOptionsi.gauge.endAngle); - // threshold values - for (var j = 0; j < gaugeOptionsi.threshold.values.length; j++) { - var threshold = gaugeOptionsi.threshold.values[j]; - if (threshold.value > gaugeOptionsi.gauge.min && threshold.value < gaugeOptionsi.gauge.max) { - var a = calculateAngle(gaugeOptionsi, layout, threshold.value); - drawThresholdValue(gaugeOptionsi, layout, cellLayout, i + "_" + j, threshold.value, a); - } - } - } - - /** - * draw the value of the threshold - * - * @method drawThresholdValue - * @param {Object} gaugeOptionsi the options of the gauge - * @param {Object} layout the layout properties - * @param {Object} cellLayout the cell layout properties - * @param {Number} i the index of the series - * @param {Number} value the value of the threshold - * @param {Number} a the angle of the value drawn - */ - function drawThresholdValue(gaugeOptionsi, layout, cellLayout, i, value, a) { - drawText( - cellLayout.cx - + ((layout.thresholdLabelMargin + (layout.thresholdLabelFontSize / 2) + layout.radius) - * Math.cos(toRad(a))), - cellLayout.cy - + ((layout.thresholdLabelMargin + (layout.thresholdLabelFontSize / 2) + layout.radius) - * Math.sin(toRad(a))), - "flotGagueThresholdValue" + i, - gaugeOptionsi.threshold.label.formatter ? gaugeOptionsi.threshold.label.formatter(value) : value, - gaugeOptionsi.threshold.label, - a); - } - - /** - * draw a text - * - * the textOptions is assumed as follows: - * - * textOptions: { - * background: { - * color: null, - * opacity: 0 - * }, - * font: { - * size: "auto" - * family: "\"MS ゴシック\",sans-serif" - * }, - * color: null - * } - * - * @method drawText - * @private - * @param {Number} x the x position of the text drawn (left top) - * @param {Number} y the y position of the text drawn (left top) - * @param {String} id the id of the dom element - * @param {String} text the text drawn - * @param {Object} textOptions the option of the text - * @param {Number} [a] the angle of the value drawn - */ - function drawText(x, y, id, text, textOptions, a) { - var span = $("." + id, placeholder); - var exists = span.length; - if (!exists) { - span = $("") - span.attr("id", id); - span.css("position", "absolute"); - span.css("top", y + "px"); - if (textOptions.font.size) { - span.css("font-size", textOptions.font.size + "px"); - } - if (textOptions.font.family) { - span.css("font-family", textOptions.font.family); - } - if (textOptions.color) { - span.css("color", textOptions.color); - } - if (textOptions.background.color) { - span.css("background-color", textOptions.background.color); - } - if (textOptions.background.opacity) { - span.css("opacity", textOptions.background.opacity); - } - placeholder.append(span); - } - span.text(text); - // after append, readjust the left position - span.css("left", x + "px"); // for redraw, resetting the left position is needed here - span.css("left", (parseInt(span.css("left")) - (span.width()/ 2)) + "px"); - - // at last, set angle - if (!exists && a) { - span.css("top", (parseInt(span.css("top")) - (span.height()/ 2)) + "px"); - span.css("transform", "rotate(" + ((180 * a) + 90) + "deg)"); // not supported for ie8 - } - } - - return Gauge; - })(); - /** - * get a instance of Logger - * - * @method getLogger - * @for flot.gauge - * @private - * @param {Object} debugOptions the options of debug - */ - function getLogger(debugOptions) { - return typeof Logger !== "undefined" ? new Logger(debugOptions) : null; - } - - /** - * calculate the index of columns for the specified data - * - * @method col - * @for flot.gauge - * @param {Number} columns the number of columns - * @param {Number} i the index of the series - * @return the index of columns - */ - function col(columns, i) { - return i % columns; - } - - /** - * calculate the index of rows for the specified data - * - * @method row - * @for flot.gauge - * @param {Number} columns the number of rows - * @param {Number} i the index of the series - * @return the index of rows - */ - function row(columns, i) { - return Math.floor(i / columns); - } - - /** - * calculate the angle in radians - * - * internally, use a number without PI (0 - 2). - * so, in this function, multiply PI - * - * @method toRad - * @for flot.gauge - * @param {Number} a the number of angle without PI - * @return the angle in radians - */ - function toRad(a) { - return a * Math.PI; - } - - /** - * draw an arc - * - * @method drawArc - * @for flot.gauge - * @param {Object} context the context of canvas - * @param {Number} cx the x position of the center - * @param {Number} cy the y position of the center - * @param {Number} r the radius of an arc - * @param {Number} w the width of an arc - * @param {Number} rd1 the start angle of an arc in radians - * @param {Number} rd2 the end angle of an arc in radians - * @param {String} lc the color of a line - * @param {Number} lw the widht of a line - * @param {String} fc the fill color of an arc - */ - function drawArc(context, cx, cy, r, w, rd1, rd2, lc, lw, fc) { - if (rd1 === rd2) { - return; - } - var counterClockwise = false; - context.save(); - context.beginPath(); - context.arc(cx, cy, r, rd1, rd2, counterClockwise); - context.lineTo(cx + (r - w) * Math.cos(rd2), - cy + (r - w) * Math.sin(rd2)); - context.arc(cx, cy, r - w, rd2, rd1, !counterClockwise); - context.closePath(); - if (lw) { - context.lineWidth = lw; - } - if (lc) { - context.strokeStyle = lc; - context.stroke(); - } - if (fc) { - context.fillStyle = fc; - context.fill(); - } - context.restore(); - } - - /** - * initialize plugin - * - * @method init - * @for flot.gauge - * @private - * @param {Object} plot a instance of plot - */ - function init (plot) { - // add processOptions hook - plot.hooks.processOptions.push(function(plot, options) { - var logger = getLogger(options.series.gauges.debug); - - - - - // turn 'grid' and 'legend' off - if (options.series.gauges.show) { - options.grid.show = false; - options.legend.show = false; - } - - // sort threshold - var thresholds = options.series.gauges.threshold.values; - - thresholds.sort(function(a, b) { - if (a.value < b.value) { - return -1; - } else if (a.value > b.value) { - return 1; - } else { - return 0; - } - }); - - - - }); - - // add draw hook - plot.hooks.draw.push(function(plot, context) { - var options = plot.getOptions(); - var gaugeOptions = options.series.gauges; - - var logger = getLogger(gaugeOptions.debug); - - - if (!gaugeOptions.show) { - return; - } - - var series = plot.getData(); - - if (!series || !series.length) { - return; // if no series were passed - } - - var gauge = new Gauge(plot, context); - - // calculate layout - var layout = gauge.calculateLayout(); - - // debug layout - if (gaugeOptions.debug.layout) { - - } - - // draw background - gauge.drawBackground(layout) - - // draw cells (label, gauge, value, threshold) - for (var i = 0; i < series.length; i++) { - var item = series[i]; - - var gaugeOptionsi = $.extend({}, gaugeOptions, item.gauges); - if (item.gauges) { - // re-calculate 'auto' values - gauge.calculateAutoValues(gaugeOptionsi, layout.cellWidth); - } - - // calculate cell layout - var cellLayout = gauge.calculateCellLayout(gaugeOptionsi, layout, i); - - // draw cell background - gauge.drawCellBackground(gaugeOptionsi, cellLayout) - // debug layout - if (gaugeOptionsi.debug.layout) { - - } - // draw label - if (gaugeOptionsi.label.show) { - gauge.drawLable(gaugeOptionsi, layout, cellLayout, i, item); - } - // draw gauge - gauge.drawGauge(gaugeOptionsi, layout, cellLayout, item.label, item.data[0][1]); - // draw threshold - if (gaugeOptionsi.threshold.show) { - gauge.drawThreshold(gaugeOptionsi, layout, cellLayout); - } - if (gaugeOptionsi.threshold.label.show) { - gauge.drawThresholdValues(gaugeOptionsi, layout, cellLayout, i) - } - // draw value - if (gaugeOptionsi.value.show) { - gauge.drawValue(gaugeOptionsi, layout, cellLayout, i, item); - } - } - }); - } - - /** - * [defaults description] - * - * @property defaults - * @type {Object} - */ - var defaults = { - series: { - gauges: { - debug: { - log: false, - layout: false, - alert: false - }, - show: false, - layout: { - margin: 5, - columns: 3, - hMargin: 5, - vMargin: 5, - square: false - }, - frame: { - show: true - }, - cell: { - background: { - color: null - }, - border: { - show: true, - color: "black", - width: 1 - }, - margin: 5, - vAlign: "middle" // 'top' or 'middle' or 'bottom' - }, - gauge: { - width: "auto", // a specified number, or 'auto' - startAngle: 0.9, // 0 - 2 factor of the radians - endAngle: 2.1, // 0 - 2 factor of the radians - min: 0, - max: 100, - background: { - color: "white" - }, - border: { - color: "lightgray", - width: 2 - }, - shadow: { - show: true, - blur: 5 - } - }, - label: { - show: true, - margin: "auto", // a specified number, or 'auto' - background: { - color: null, - opacity: 0 - }, - font: { - size: "auto", // a specified number, or 'auto' - family: "sans-serif" - }, - color: null, - formatter: function(label, value) { - return label; - } - }, - value: { - show: true, - margin: "auto", // a specified number, or 'auto' - background: { - color: null, - opacity: 0 - }, - font: { - size: "auto", // a specified number, or 'auto' - family: "sans-serif" - }, - color: null, - formatter: function(label, value) { - return parseInt(value); - } - }, - threshold: { - show: true, - width: "auto", // a specified number, or 'auto' - label: { - show: true, - margin: "auto", // a specified number, or 'auto' - background: { - color: null, - opacity: 0 - }, - font: { - size: "auto", // a specified number, or 'auto' - family: ",sans-serif" - }, - color: null, - formatter: function(value) { - return value; - } - }, - values: [ - { - value: 50, - color: "lightgreen" - }, { - value: 80, - color: "yellow" - }, { - value: 100, - color: "red" - } - ] - } - } - } - }; - - // register the gauge plugin - $.plot.plugins.push({ - init: init, - options: defaults, - name: "gauge", - version: "1.1.0" - }); - -})(jQuery); From 1dfeb192a31f8a6fcfc9265a4ab99a25da3b8b74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 22 Apr 2016 13:59:54 +0200 Subject: [PATCH 29/47] fix(templating): another fix for templating and custom all value escaping, fixes #4787 --- public/app/features/templating/templateValuesSrv.js | 5 ----- public/test/specs/templateValuesSrv-specs.js | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/public/app/features/templating/templateValuesSrv.js b/public/app/features/templating/templateValuesSrv.js index 799e27583d5..4cda10ac406 100644 --- a/public/app/features/templating/templateValuesSrv.js +++ b/public/app/features/templating/templateValuesSrv.js @@ -294,11 +294,6 @@ function (angular, _, kbn) { }; this.addAllOption = function(variable) { - if (variable.allValue) { - variable.options.unshift({text: 'All', value: variable.allValue}); - return; - } - variable.options.unshift({text: 'All', value: "$__all"}); }; diff --git a/public/test/specs/templateValuesSrv-specs.js b/public/test/specs/templateValuesSrv-specs.js index d81ce4fd059..110cb548467 100644 --- a/public/test/specs/templateValuesSrv-specs.js +++ b/public/test/specs/templateValuesSrv-specs.js @@ -280,7 +280,7 @@ define([ }); it('should add All option with custom value', function() { - expect(scenario.variable.options[0].value).to.be('*'); + expect(scenario.variable.options[0].value).to.be('$__all'); }); }); From 45dd9c5795600e2a0ff21ae9cd76a7edffdbd813 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 22 Apr 2016 14:36:20 +0200 Subject: [PATCH 30/47] fix(graph panel): fix for graph panel alignment when legend is in table mode, fixes #4772 --- public/app/plugins/panel/graph/graph.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/panel/graph/graph.js b/public/app/plugins/panel/graph/graph.js index 9e6633b5551..669fe1fbc91 100755 --- a/public/app/plugins/panel/graph/graph.js +++ b/public/app/plugins/panel/graph/graph.js @@ -73,7 +73,7 @@ function (angular, $, moment, _, kbn, GraphTooltip) { var legendSeries = _.filter(data, function(series) { return series.hideFromLegend(panel.legend) === false; }); - var total = 23 + (22 * legendSeries.length); + var total = 23 + (21 * legendSeries.length); return Math.min(total, Math.floor(panelHeight/2)); } else { return 26; From 723bfa70db64f63286dffd65ff5d40fdf14f910d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 22 Apr 2016 14:46:32 +0200 Subject: [PATCH 31/47] fix(table panel): fixed issue with string array formating, fixes #4791 --- CHANGELOG.md | 1 + public/app/plugins/panel/table/renderer.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90217553c19..ba8e6242243 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * **Templating**: Fixed issue with custom all value and escaping, fixes [#4736](https://github.com/grafana/grafana/issues/4736) * **Dashlist**: Fixed issue dashboard list panel and caching tags, fixes [#4768](https://github.com/grafana/grafana/issues/4768) * **Graph**: Fixed issue with unneeded scrollbar in legend for Firefox, fixes [#4760](https://github.com/grafana/grafana/issues/4760) +* **Table panel**: Fixed issue table panel formating string array properties, fixes [#4791](https://github.com/grafana/grafana/issues/4791) # 3.0.0-beta5 (2016-04-15) diff --git a/public/app/plugins/panel/table/renderer.ts b/public/app/plugins/panel/table/renderer.ts index c00656071be..6d75f5c97c9 100644 --- a/public/app/plugins/panel/table/renderer.ts +++ b/public/app/plugins/panel/table/renderer.ts @@ -30,7 +30,7 @@ export class TableRenderer { } if (_.isArray(v)) { - v = v.join(', '); + v = v.join(', '); } return v; From 453f8b312a983a09e4ee21f8ecf7ae951bee411e Mon Sep 17 00:00:00 2001 From: Carl Bergquist Date: Fri, 22 Apr 2016 14:55:35 +0200 Subject: [PATCH 32/47] Revert "Revert "Gauges"" --- .../app/plugins/panel/singlestat/editor.html | 37 + public/app/plugins/panel/singlestat/module.ts | 95 +- public/app/system.conf.js | 3 +- public/test/test-main.js | 3 +- public/vendor/flot/jquery.flot.gauge.js | 960 ++++++++++++++++++ 5 files changed, 1094 insertions(+), 4 deletions(-) create mode 100644 public/vendor/flot/jquery.flot.gauge.js diff --git a/public/app/plugins/panel/singlestat/editor.html b/public/app/plugins/panel/singlestat/editor.html index 6b0806133b8..437445a2400 100644 --- a/public/app/plugins/panel/singlestat/editor.html +++ b/public/app/plugins/panel/singlestat/editor.html @@ -156,6 +156,43 @@
+
+
+
+
    +
  • + Gauge +
  • +
  • + Show  + + +
  • +
  • + Threshold labels  + + +
  • +
  • + Min +
  • +
  • + +
  • +
  • + Max +
  • +
  • + +
  • +
+
+
+
+
+
diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index 8af684c9d68..6d60a70269e 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -4,8 +4,10 @@ import angular from 'angular'; import _ from 'lodash'; import $ from 'jquery'; import 'jquery.flot'; +import 'jquery.flot.gauge'; import kbn from 'app/core/utils/kbn'; +import config from 'app/core/config'; import TimeSeries from 'app/core/time_series2'; import {MetricsPanelCtrl} from 'app/plugins/sdk'; @@ -38,6 +40,12 @@ var panelDefaults = { full: false, lineColor: 'rgb(31, 120, 193)', fillColor: 'rgba(31, 118, 189, 0.18)', + }, + gauge: { + show: false, + minValue: 0, + maxValue: 100, + thresholdLabels: true } }; @@ -270,6 +278,86 @@ class SingleStatCtrl extends MetricsPanelCtrl { return body; } + function addGauge() { + var plotCanvas = $('
'); + var plotCss = { + top: '10px', + margin: 'auto', + position: 'relative', + height: (elem.height() * 0.9) + 'px', + width: elem.width() + 'px' + }; + + plotCanvas.css(plotCss); + + var thresholds = []; + for (var i = 0; i < data.thresholds.length; i++) { + thresholds.push({ + value: data.thresholds[i], + color: data.colorMap[i] + }); + } + thresholds.push({ + value: panel.gauge.maxValue, + color: data.colorMap[data.colorMap.length - 1] + }); + + var bgColor = config.bootData.user.lightTheme + ? 'rgb(230,230,230)' + : 'rgb(38,38,38)'; + + var options = { + series: { + gauges: { + gauge: { + min: panel.gauge.minValue, + max: panel.gauge.maxValue, + background: { color: bgColor }, + border: { color: null }, + shadow: { show: false }, + width: 38 + }, + frame: { show: false }, + label: { show: false }, + layout: { margin: 0 }, + cell: { border: { width: 0 } }, + threshold: { + values: thresholds, + label: { + show: panel.gauge.thresholdLabels, + margin: 8, + font: { size: 18 } + }, + width: 8 + }, + value: { + color: panel.colorValue ? getColorForValue(data, data.valueRounded) : null, + formatter: function () { return data.valueFormated; }, + font: { size: getGaugeFontSize() } + }, + show: true + } + } + }; + + elem.append(plotCanvas); + + var plotSeries = { + data: [[0, data.valueRounded]] + }; + + $.plot(plotCanvas, [plotSeries], options); + } + + function getGaugeFontSize() { + if (panel.valueFontSize) { + var num = parseInt(panel.valueFontSize.substring(0, panel.valueFontSize.length - 1)); + return 30 * (num / 100); + } else { + return 30; + } + } + function addSparkline() { var width = elem.width() + 20; if (width < 30) { @@ -331,11 +419,10 @@ class SingleStatCtrl extends MetricsPanelCtrl { function render() { if (!ctrl.data) { return; } - data = ctrl.data; setElementHeight(); - var body = getBigValueHtml(); + var body = panel.gauge.show ? '' : getBigValueHtml(); if (panel.colorBackground && !isNaN(data.valueRounded)) { var color = getColorForValue(data, data.valueRounded); @@ -358,6 +445,10 @@ class SingleStatCtrl extends MetricsPanelCtrl { addSparkline(); } + if (panel.gauge.show) { + addGauge(); + } + elem.toggleClass('pointer', panel.links.length > 0); if (panel.links.length > 0) { diff --git a/public/app/system.conf.js b/public/app/system.conf.js index 276988e5c34..40b70c0e30e 100644 --- a/public/app/system.conf.js +++ b/public/app/system.conf.js @@ -27,7 +27,8 @@ System.config({ "jquery.flot.stackpercent": "vendor/flot/jquery.flot.stackpercent", "jquery.flot.time": "vendor/flot/jquery.flot.time", "jquery.flot.crosshair": "vendor/flot/jquery.flot.crosshair", - "jquery.flot.fillbelow": "vendor/flot/jquery.flot.fillbelow" + "jquery.flot.fillbelow": "vendor/flot/jquery.flot.fillbelow", + "jquery.flot.gauge": "vendor/flot/jquery.flot.gauge" }, packages: { diff --git a/public/test/test-main.js b/public/test/test-main.js index d40955022fe..dbee086c250 100644 --- a/public/test/test-main.js +++ b/public/test/test-main.js @@ -35,7 +35,8 @@ "jquery.flot.stackpercent": "vendor/flot/jquery.flot.stackpercent", "jquery.flot.time": "vendor/flot/jquery.flot.time", "jquery.flot.crosshair": "vendor/flot/jquery.flot.crosshair", - "jquery.flot.fillbelow": "vendor/flot/jquery.flot.fillbelow" + "jquery.flot.fillbelow": "vendor/flot/jquery.flot.fillbelow", + "jquery.flot.gauge": "vendor/flot/jquery.flot.gauge" }, packages: { diff --git a/public/vendor/flot/jquery.flot.gauge.js b/public/vendor/flot/jquery.flot.gauge.js new file mode 100644 index 00000000000..d8ed958e990 --- /dev/null +++ b/public/vendor/flot/jquery.flot.gauge.js @@ -0,0 +1,960 @@ +/*! + * jquery.flot.gauge v1.1.0 * + * + * Flot plugin for rendering gauge charts. + * + * Copyright (c) 2015 @toyoty99. + * Licensed under the MIT license. + */ + +/** + * @module flot.gauge + */ +(function($) { + + + /** + * Gauge class + * + * @class Gauge + */ + var Gauge = (function() { + /** + * context of canvas + * + * @property context + * @type Object + */ + var context; + /** + * placeholder of canvas + * + * @property placeholder + * @type Object + */ + var placeholder; + /** + * options of plot + * + * @property options + * @type Object + */ + var options; + /** + * options of gauge + * + * @property gaugeOptions + * @type Object + */ + var gaugeOptions; + /** + * data series + * + * @property series + * @type Array + */ + var series; + /** + * logger + * + * @property logger + * @type Object + */ + var logger; + + /** + * constructor + * + * @class Gauge + * @constructor + * @param {Object} gaugeOptions gauge options + */ + var Gauge = function(plot, ctx) { + context = ctx; + placeholder = plot.getPlaceholder(); + options = plot.getOptions(); + gaugeOptions = options.series.gauges; + series = plot.getData(); + logger = getLogger(gaugeOptions.debug); + } + + /** + * calculate layout + * + * @method calculateLayout + * @return the calculated layout properties + */ + Gauge.prototype.calculateLayout = function() { + + var canvasWidth = placeholder.width(); + var canvasHeight = placeholder.height(); + + + + // calculate cell size + var columns = Math.min(series.length, gaugeOptions.layout.columns); + var rows = Math.ceil(series.length / columns); + + + + var margin = gaugeOptions.layout.margin; + var hMargin = gaugeOptions.layout.hMargin; + var vMargin = gaugeOptions.layout.vMargin; + var cellWidth = (canvasWidth - (margin * 2) - (hMargin * (columns - 1))) / columns; + var cellHeight = (canvasHeight - (margin * 2) - (vMargin * (rows - 1))) / rows; + if (gaugeOptions.layout.square) { + var cell = Math.min(cellWidth, cellHeight); + cellWidth = cell; + cellHeight = cell; + } + + + + // calculate 'auto' values + calculateAutoValues(gaugeOptions, cellWidth); + + // calculate maximum radius + var cellMargin = gaugeOptions.cell.margin; + var labelMargin = 0; + var labelFontSize = 0; + if (gaugeOptions.label.show) { + labelMargin = gaugeOptions.label.margin; + labelFontSize = gaugeOptions.label.font.size; + } + var valueMargin = 0; + var valueFontSize = 0; + if (gaugeOptions.value.show) { + valueMargin = gaugeOptions.value.margin; + valueFontSize = gaugeOptions.value.font.size; + } + var thresholdWidth = 0; + if (gaugeOptions.threshold.show) { + thresholdWidth = gaugeOptions.threshold.width; + } + var thresholdLabelMargin = 0; + var thresholdLabelFontSize = 0; + if (gaugeOptions.threshold.label.show) { + thresholdLabelMargin = gaugeOptions.threshold.label.margin; + thresholdLabelFontSize = gaugeOptions.threshold.label.font.size; + } + + var maxRadiusH = (cellWidth / 2) - cellMargin - thresholdWidth - (thresholdLabelMargin * 2) - thresholdLabelFontSize; + + var startAngle = gaugeOptions.gauge.startAngle; + var endAngle = gaugeOptions.gauge.endAngle; + var dAngle = (endAngle - startAngle) / 100; + var heightRatioV = -1; + for (var a = startAngle; a < endAngle; a += dAngle) { + heightRatioV = Math.max(heightRatioV, Math.sin(toRad(a))); + } + heightRatioV = Math.max(heightRatioV, Math.sin(toRad(endAngle))); + var outerRadiusV = (cellHeight - (cellMargin * 2) - (labelMargin * 2) - labelFontSize) / (1 + heightRatioV); + if (outerRadiusV * heightRatioV < valueMargin + (valueFontSize / 2)) { + outerRadiusV = cellHeight - (cellMargin * 2) - (labelMargin * 2) - labelFontSize - valueMargin - (valueFontSize / 2); + } + var maxRadiusV = outerRadiusV - (thresholdLabelMargin * 2) - thresholdLabelFontSize - thresholdWidth; + + var radius = Math.min(maxRadiusH, maxRadiusV); + + + var width = gaugeOptions.gauge.width; + if (width >= radius) { + width = Math.max(3, radius / 3); + } + + + var outerRadius = (thresholdLabelMargin * 2) + thresholdLabelFontSize + thresholdWidth + radius; + var gaugeOuterHeight = Math.max(outerRadius * (1 + heightRatioV), outerRadius + valueMargin + (valueFontSize / 2)); + + return { + canvasWidth: canvasWidth, + canvasHeight: canvasHeight, + margin: margin, + hMargin: hMargin, + vMargin: vMargin, + columns: columns, + rows: rows, + cellWidth: cellWidth, + cellHeight: cellHeight, + cellMargin: cellMargin, + labelMargin: labelMargin, + labelFontSize: labelFontSize, + valueMargin: valueMargin, + valueFontSize: valueFontSize, + width: width, + radius: radius, + thresholdWidth: thresholdWidth, + thresholdLabelMargin: thresholdLabelMargin, + thresholdLabelFontSize: thresholdLabelFontSize, + gaugeOuterHeight: gaugeOuterHeight + }; + } + + /** + * calculate the values which are set as 'auto' + * + * @method calculateAutoValues + * @param {Object} gaugeOptionsi the options of the gauge + * @param {Number} cellWidth the width of cell + */ + function calculateAutoValues(gaugeOptionsi, cellWidth) { + + if (gaugeOptionsi.gauge.width === "auto") { + gaugeOptionsi.gauge.width = Math.max(5, cellWidth / 8); + } + if (gaugeOptionsi.label.margin === "auto") { + gaugeOptionsi.label.margin = Math.max(1, cellWidth / 20); + } + if (gaugeOptionsi.label.font.size === "auto") { + gaugeOptionsi.label.font.size = Math.max(5, cellWidth / 8); + } + if (gaugeOptionsi.value.margin === "auto") { + gaugeOptionsi.value.margin = Math.max(1, cellWidth / 30); + } + if (gaugeOptionsi.value.font.size === "auto") { + gaugeOptionsi.value.font.size = Math.max(5, cellWidth / 9); + } + if (gaugeOptionsi.threshold.width === "auto") { + gaugeOptionsi.threshold.width = Math.max(3, cellWidth / 100); + } + if (gaugeOptionsi.threshold.label.margin === "auto") { + gaugeOptionsi.threshold.label.margin = Math.max(3, cellWidth / 40); + } + if (gaugeOptionsi.threshold.label.font.size === "auto") { + gaugeOptionsi.threshold.label.font.size = Math.max(5, cellWidth / 15); + } + + } + Gauge.prototype.calculateAutoValues = calculateAutoValues; + + /** + * calculate the layout of the cell inside + * + * @method calculateCellLayout + * @param {Object} gaugeOptionsi the options of the gauge + * @param {Number} cellWidth the width of cell + * @param {Number} i the index of the series + * @return the calculated cell layout properties + */ + Gauge.prototype.calculateCellLayout = function(gaugeOptionsi, layout, i) { + + // calculate top, left and center + var c = col(layout.columns, i); + var r = row(layout.columns, i); + var x = layout.margin + (layout.cellWidth + layout.hMargin) * c; + var y = layout.margin + (layout.cellHeight + layout.vMargin) * r; + var cx = x + (layout.cellWidth / 2); + var cy = y + layout.cellMargin + (layout.labelMargin * 2) + layout.labelFontSize + layout.thresholdWidth + + layout.thresholdLabelFontSize + (layout.thresholdLabelMargin * 2) + layout.radius; + var blank = layout.cellHeight - (layout.cellMargin * 2) - (layout.labelMargin * 2) - layout.labelFontSize - layout.gaugeOuterHeight; + var offsetY = 0; + if (gaugeOptionsi.cell.vAlign === "middle") { + offsetY = (blank / 2); + } else if (gaugeOptionsi.cell.vAlign === "bottom") { + offsetY = blank; + } + cy += offsetY; + + return { + col: c, + row: r, + x: x, + y: y, + offsetY: offsetY, + cellWidth: layout.cellWidth, + cellHeight: layout.cellHeight, + cellMargin: layout.cellMargin, + cx: cx, + cy: cy + } + } + + /** + * draw the background of chart + * + * @method drawBackground + * @param {Object} layout the layout properties + */ + Gauge.prototype.drawBackground = function(layout) { + + if (!gaugeOptions.frame.show) { + return; + } + context.save(); + context.strokeStyle = options.grid.borderColor; + context.lineWidth = options.grid.borderWidth; + context.strokeRect(0, 0, layout.canvasWidth, layout.canvasHeight); + if (options.grid.backgroundColor) { + context.fillStyle = options.grid.backgroundColor; + context.fillRect(0, 0, layout.canvasWidth, layout.canvasHeight); + } + context.restore(); + } + + /** + * draw the background of cell + * + * @method drawCellBackground + * @param {Object} gaugeOptionsi the options of the gauge + * @param {Object} cellLayout the cell layout properties + */ + Gauge.prototype.drawCellBackground = function(gaugeOptionsi, cellLayout) { + + context.save(); + if (gaugeOptionsi.cell.border && gaugeOptionsi.cell.border.show && gaugeOptionsi.cell.border.color && gaugeOptionsi.cell.border.width) { + context.strokeStyle = gaugeOptionsi.cell.border.color; + context.lineWidth = gaugeOptionsi.cell.border.width; + context.strokeRect(cellLayout.x, cellLayout.y, cellLayout.cellWidth, cellLayout.cellHeight); + } + if (gaugeOptionsi.cell.background && gaugeOptionsi.cell.background.color) { + context.fillStyle = gaugeOptionsi.cell.background.color; + context.fillRect(cellLayout.x, cellLayout.y, cellLayout.cellWidth, cellLayout.cellHeight); + } + context.restore(); + } + + /** + * draw the gauge + * + * @method drawGauge + * @param {Object} gaugeOptionsi the options of the gauge + * @param {Object} layout the layout properties + * @param {Object} cellLayout the cell layout properties + * @param {String} label the label of data + * @param {Number} data the value of the gauge + */ + Gauge.prototype.drawGauge = function(gaugeOptionsi, layout, cellLayout, label, data) { + + + var blur = gaugeOptionsi.gauge.shadow.show ? gaugeOptionsi.gauge.shadow.blur : 0; + + + // draw gauge frame + drawArcWithShadow( + cellLayout.cx, // center x + cellLayout.cy, // center y + layout.radius, + layout.width, + toRad(gaugeOptionsi.gauge.startAngle), + toRad(gaugeOptionsi.gauge.endAngle), + gaugeOptionsi.gauge.border.color, // line color + gaugeOptionsi.gauge.border.width, // line width + gaugeOptionsi.gauge.background.color, // fill color + blur); + + // draw gauge + var c1 = getColor(gaugeOptionsi, data); + var a2 = calculateAngle(gaugeOptionsi, layout, data); + drawArcWithShadow( + cellLayout.cx, // center x + cellLayout.cy, // center y + layout.radius - 1, + layout.width - 2, + toRad(gaugeOptionsi.gauge.startAngle), + toRad(a2), + c1, // line color + 1, // line width + c1, // fill color + blur); + } + + /** + * decide the color of the data from the threshold options + * + * @method getColor + * @private + * @param {Object} gaugeOptionsi the options of the gauge + * @param {Number} data the value of the gauge + */ + function getColor(gaugeOptionsi, data) { + var color; + for (var i = 0; i < gaugeOptionsi.threshold.values.length; i++) { + var threshold = gaugeOptionsi.threshold.values[i]; + color = threshold.color; + if (data <= threshold.value) { + break; + } + } + return color; + } + + /** + * calculate the angle of the data + * + * @method calculateAngle + * @private + * @param {Object} gaugeOptionsi the options of the gauge + * @param {Object} layout the layout properties + * @param {Number} data the value of the gauge + */ + function calculateAngle(gaugeOptionsi, layout, data) { + var a = + gaugeOptionsi.gauge.startAngle + + (gaugeOptionsi.gauge.endAngle - gaugeOptionsi.gauge.startAngle) + * ((data - gaugeOptionsi.gauge.min) / (gaugeOptionsi.gauge.max - gaugeOptionsi.gauge.min)); + + if (a < gaugeOptionsi.gauge.startAngle) { + a = gaugeOptionsi.gauge.startAngle; + } else if (a > gaugeOptionsi.gauge.endAngle) { + a = gaugeOptionsi.gauge.endAngle; + } + return a; + } + + /** + * draw the arc of the threshold + * + * @method drawThreshold + * @param {Object} gaugeOptionsi the options of the gauge + * @param {Object} layout the layout properties + * @param {Object} cellLayout the cell layout properties + */ + Gauge.prototype.drawThreshold = function(gaugeOptionsi, layout, cellLayout) { + + var a1 = gaugeOptionsi.gauge.startAngle; + for (var i = 0; i < gaugeOptionsi.threshold.values.length; i++) { + var threshold = gaugeOptionsi.threshold.values[i]; + c1 = threshold.color; + a2 = calculateAngle(gaugeOptionsi, layout, threshold.value); + drawArc( + context, + cellLayout.cx, // center x + cellLayout.cy, // center y + layout.radius + layout.thresholdWidth, + layout.thresholdWidth - 2, + toRad(a1), + toRad(a2), + c1, // line color + 1, // line width + c1); // fill color + a1 = a2; + } + } + + /** + * draw an arc with a shadow + * + * @method drawArcWithShadow + * @private + * @param {Number} cx the x position of the center + * @param {Number} cy the y position of the center + * @param {Number} r the radius of an arc + * @param {Number} w the width of an arc + * @param {Number} rd1 the start angle of an arc in radians + * @param {Number} rd2 the end angle of an arc in radians + * @param {String} lc the color of a line + * @param {Number} lw the widht of a line + * @param {String} fc the fill color of an arc + * @param {Number} blur the shdow blur + */ + function drawArcWithShadow(cx, cy, r, w, rd1, rd2, lc, lw, fc, blur) { + if (rd1 === rd2) { + return; + } + context.save(); + + drawArc(context, cx, cy, r, w, rd1, rd2, lc, lw, fc); + + if (blur) { + drawArc(context, cx, cy, r, w, rd1, rd2); + context.clip(); + context.shadowOffsetX = 0; + context.shadowOffsetY = 0; + context.shadowBlur = 10; + context.shadowColor = "gray"; + drawArc(context, cx, cy, r + 1, w + 2, rd1, rd2, lc, 1); + } + context.restore(); + } + + /** + * draw the label of the gauge + * + * @method drawLable + * @param {Object} gaugeOptionsi the options of the gauge + * @param {Object} layout the layout properties + * @param {Object} cellLayout the cell layout properties + * @param {Number} i the index of the series + * @param {Object} item the item of the series + */ + Gauge.prototype.drawLable = function(gaugeOptionsi, layout, cellLayout, i, item) { + + drawText( + cellLayout.cx, + cellLayout.y + cellLayout.cellMargin + layout.labelMargin + cellLayout.offsetY, + "flotGagueLabel" + i, + gaugeOptionsi.label.formatter ? gaugeOptionsi.label.formatter(item.label, item.data[0][1]) : text, + gaugeOptionsi.label); + } + + /** + * draw the value of the gauge + * + * @method drawValue + * @param {Object} gaugeOptionsi the options of the gauge + * @param {Object} layout the layout properties + * @param {Object} cellLayout the cell layout properties + * @param {Number} i the index of the series + * @param {Object} item the item of the series + */ + Gauge.prototype.drawValue = function(gaugeOptionsi, layout, cellLayout, i, item) { + + drawText( + cellLayout.cx, + cellLayout.cy - (gaugeOptionsi.value.font.size / 2), + "flotGagueValue" + i, + gaugeOptionsi.value.formatter ? gaugeOptionsi.value.formatter(item.label, item.data[0][1]) : text, + gaugeOptionsi.value); + } + + /** + * draw the values of the threshold + * + * @method drawThresholdValues + * @param {Object} gaugeOptionsi the options of the gauge + * @param {Object} layout the layout properties + * @param {Object} cellLayout the cell layout properties + * @param {Number} i the index of the series + */ + Gauge.prototype.drawThresholdValues = function(gaugeOptionsi, layout, cellLayout, i) { + + // min, max + drawThresholdValue(gaugeOptionsi, layout, cellLayout, "Min" + i, gaugeOptionsi.gauge.min, gaugeOptionsi.gauge.startAngle); + drawThresholdValue(gaugeOptionsi, layout, cellLayout, "Max" + i, gaugeOptionsi.gauge.max, gaugeOptionsi.gauge.endAngle); + // threshold values + for (var j = 0; j < gaugeOptionsi.threshold.values.length; j++) { + var threshold = gaugeOptionsi.threshold.values[j]; + if (threshold.value > gaugeOptionsi.gauge.min && threshold.value < gaugeOptionsi.gauge.max) { + var a = calculateAngle(gaugeOptionsi, layout, threshold.value); + drawThresholdValue(gaugeOptionsi, layout, cellLayout, i + "_" + j, threshold.value, a); + } + } + } + + /** + * draw the value of the threshold + * + * @method drawThresholdValue + * @param {Object} gaugeOptionsi the options of the gauge + * @param {Object} layout the layout properties + * @param {Object} cellLayout the cell layout properties + * @param {Number} i the index of the series + * @param {Number} value the value of the threshold + * @param {Number} a the angle of the value drawn + */ + function drawThresholdValue(gaugeOptionsi, layout, cellLayout, i, value, a) { + drawText( + cellLayout.cx + + ((layout.thresholdLabelMargin + (layout.thresholdLabelFontSize / 2) + layout.radius) + * Math.cos(toRad(a))), + cellLayout.cy + + ((layout.thresholdLabelMargin + (layout.thresholdLabelFontSize / 2) + layout.radius) + * Math.sin(toRad(a))), + "flotGagueThresholdValue" + i, + gaugeOptionsi.threshold.label.formatter ? gaugeOptionsi.threshold.label.formatter(value) : value, + gaugeOptionsi.threshold.label, + a); + } + + /** + * draw a text + * + * the textOptions is assumed as follows: + * + * textOptions: { + * background: { + * color: null, + * opacity: 0 + * }, + * font: { + * size: "auto" + * family: "\"MS ゴシック\",sans-serif" + * }, + * color: null + * } + * + * @method drawText + * @private + * @param {Number} x the x position of the text drawn (left top) + * @param {Number} y the y position of the text drawn (left top) + * @param {String} id the id of the dom element + * @param {String} text the text drawn + * @param {Object} textOptions the option of the text + * @param {Number} [a] the angle of the value drawn + */ + function drawText(x, y, id, text, textOptions, a) { + var span = $("." + id, placeholder); + var exists = span.length; + if (!exists) { + span = $("") + span.attr("id", id); + span.css("position", "absolute"); + span.css("top", y + "px"); + if (textOptions.font.size) { + span.css("font-size", textOptions.font.size + "px"); + } + if (textOptions.font.family) { + span.css("font-family", textOptions.font.family); + } + if (textOptions.color) { + span.css("color", textOptions.color); + } + if (textOptions.background.color) { + span.css("background-color", textOptions.background.color); + } + if (textOptions.background.opacity) { + span.css("opacity", textOptions.background.opacity); + } + placeholder.append(span); + } + span.text(text); + // after append, readjust the left position + span.css("left", x + "px"); // for redraw, resetting the left position is needed here + span.css("left", (parseInt(span.css("left")) - (span.width()/ 2)) + "px"); + + // at last, set angle + if (!exists && a) { + span.css("top", (parseInt(span.css("top")) - (span.height()/ 2)) + "px"); + span.css("transform", "rotate(" + ((180 * a) + 90) + "deg)"); // not supported for ie8 + } + } + + return Gauge; + })(); + /** + * get a instance of Logger + * + * @method getLogger + * @for flot.gauge + * @private + * @param {Object} debugOptions the options of debug + */ + function getLogger(debugOptions) { + return typeof Logger !== "undefined" ? new Logger(debugOptions) : null; + } + + /** + * calculate the index of columns for the specified data + * + * @method col + * @for flot.gauge + * @param {Number} columns the number of columns + * @param {Number} i the index of the series + * @return the index of columns + */ + function col(columns, i) { + return i % columns; + } + + /** + * calculate the index of rows for the specified data + * + * @method row + * @for flot.gauge + * @param {Number} columns the number of rows + * @param {Number} i the index of the series + * @return the index of rows + */ + function row(columns, i) { + return Math.floor(i / columns); + } + + /** + * calculate the angle in radians + * + * internally, use a number without PI (0 - 2). + * so, in this function, multiply PI + * + * @method toRad + * @for flot.gauge + * @param {Number} a the number of angle without PI + * @return the angle in radians + */ + function toRad(a) { + return a * Math.PI; + } + + /** + * draw an arc + * + * @method drawArc + * @for flot.gauge + * @param {Object} context the context of canvas + * @param {Number} cx the x position of the center + * @param {Number} cy the y position of the center + * @param {Number} r the radius of an arc + * @param {Number} w the width of an arc + * @param {Number} rd1 the start angle of an arc in radians + * @param {Number} rd2 the end angle of an arc in radians + * @param {String} lc the color of a line + * @param {Number} lw the widht of a line + * @param {String} fc the fill color of an arc + */ + function drawArc(context, cx, cy, r, w, rd1, rd2, lc, lw, fc) { + if (rd1 === rd2) { + return; + } + var counterClockwise = false; + context.save(); + context.beginPath(); + context.arc(cx, cy, r, rd1, rd2, counterClockwise); + context.lineTo(cx + (r - w) * Math.cos(rd2), + cy + (r - w) * Math.sin(rd2)); + context.arc(cx, cy, r - w, rd2, rd1, !counterClockwise); + context.closePath(); + if (lw) { + context.lineWidth = lw; + } + if (lc) { + context.strokeStyle = lc; + context.stroke(); + } + if (fc) { + context.fillStyle = fc; + context.fill(); + } + context.restore(); + } + + /** + * initialize plugin + * + * @method init + * @for flot.gauge + * @private + * @param {Object} plot a instance of plot + */ + function init (plot) { + // add processOptions hook + plot.hooks.processOptions.push(function(plot, options) { + var logger = getLogger(options.series.gauges.debug); + + + + + // turn 'grid' and 'legend' off + if (options.series.gauges.show) { + options.grid.show = false; + options.legend.show = false; + } + + // sort threshold + var thresholds = options.series.gauges.threshold.values; + + thresholds.sort(function(a, b) { + if (a.value < b.value) { + return -1; + } else if (a.value > b.value) { + return 1; + } else { + return 0; + } + }); + + + + }); + + // add draw hook + plot.hooks.draw.push(function(plot, context) { + var options = plot.getOptions(); + var gaugeOptions = options.series.gauges; + + var logger = getLogger(gaugeOptions.debug); + + + if (!gaugeOptions.show) { + return; + } + + var series = plot.getData(); + + if (!series || !series.length) { + return; // if no series were passed + } + + var gauge = new Gauge(plot, context); + + // calculate layout + var layout = gauge.calculateLayout(); + + // debug layout + if (gaugeOptions.debug.layout) { + + } + + // draw background + gauge.drawBackground(layout) + + // draw cells (label, gauge, value, threshold) + for (var i = 0; i < series.length; i++) { + var item = series[i]; + + var gaugeOptionsi = $.extend({}, gaugeOptions, item.gauges); + if (item.gauges) { + // re-calculate 'auto' values + gauge.calculateAutoValues(gaugeOptionsi, layout.cellWidth); + } + + // calculate cell layout + var cellLayout = gauge.calculateCellLayout(gaugeOptionsi, layout, i); + + // draw cell background + gauge.drawCellBackground(gaugeOptionsi, cellLayout) + // debug layout + if (gaugeOptionsi.debug.layout) { + + } + // draw label + if (gaugeOptionsi.label.show) { + gauge.drawLable(gaugeOptionsi, layout, cellLayout, i, item); + } + // draw gauge + gauge.drawGauge(gaugeOptionsi, layout, cellLayout, item.label, item.data[0][1]); + // draw threshold + if (gaugeOptionsi.threshold.show) { + gauge.drawThreshold(gaugeOptionsi, layout, cellLayout); + } + if (gaugeOptionsi.threshold.label.show) { + gauge.drawThresholdValues(gaugeOptionsi, layout, cellLayout, i) + } + // draw value + if (gaugeOptionsi.value.show) { + gauge.drawValue(gaugeOptionsi, layout, cellLayout, i, item); + } + } + }); + } + + /** + * [defaults description] + * + * @property defaults + * @type {Object} + */ + var defaults = { + series: { + gauges: { + debug: { + log: false, + layout: false, + alert: false + }, + show: false, + layout: { + margin: 5, + columns: 3, + hMargin: 5, + vMargin: 5, + square: false + }, + frame: { + show: true + }, + cell: { + background: { + color: null + }, + border: { + show: true, + color: "black", + width: 1 + }, + margin: 5, + vAlign: "middle" // 'top' or 'middle' or 'bottom' + }, + gauge: { + width: "auto", // a specified number, or 'auto' + startAngle: 0.9, // 0 - 2 factor of the radians + endAngle: 2.1, // 0 - 2 factor of the radians + min: 0, + max: 100, + background: { + color: "white" + }, + border: { + color: "lightgray", + width: 2 + }, + shadow: { + show: true, + blur: 5 + } + }, + label: { + show: true, + margin: "auto", // a specified number, or 'auto' + background: { + color: null, + opacity: 0 + }, + font: { + size: "auto", // a specified number, or 'auto' + family: "sans-serif" + }, + color: null, + formatter: function(label, value) { + return label; + } + }, + value: { + show: true, + margin: "auto", // a specified number, or 'auto' + background: { + color: null, + opacity: 0 + }, + font: { + size: "auto", // a specified number, or 'auto' + family: "sans-serif" + }, + color: null, + formatter: function(label, value) { + return parseInt(value); + } + }, + threshold: { + show: true, + width: "auto", // a specified number, or 'auto' + label: { + show: true, + margin: "auto", // a specified number, or 'auto' + background: { + color: null, + opacity: 0 + }, + font: { + size: "auto", // a specified number, or 'auto' + family: ",sans-serif" + }, + color: null, + formatter: function(value) { + return value; + } + }, + values: [ + { + value: 50, + color: "lightgreen" + }, { + value: 80, + color: "yellow" + }, { + value: 100, + color: "red" + } + ] + } + } + } + }; + + // register the gauge plugin + $.plot.plugins.push({ + init: init, + options: defaults, + name: "gauge", + version: "1.1.0" + }); + +})(jQuery); From 8b656f15b9274365d99c7f7d2dc73fbcc21fc2ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 22 Apr 2016 14:57:08 +0200 Subject: [PATCH 33/47] feat(graphite): added stddevSeries func def, closes #4782 --- public/app/plugins/datasource/graphite/gfunc.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/public/app/plugins/datasource/graphite/gfunc.js b/public/app/plugins/datasource/graphite/gfunc.js index de482598fc5..a7907186811 100644 --- a/public/app/plugins/datasource/graphite/gfunc.js +++ b/public/app/plugins/datasource/graphite/gfunc.js @@ -80,6 +80,13 @@ function (_, $) { category: categories.Calculate, }); + addFuncDef({ + name: 'stddevSeries', + params: optionalSeriesRefArgs, + defaultParams: [''], + category: categories.Calculate, + }); + addFuncDef({ name: 'divideSeries', params: optionalSeriesRefArgs, From 16dbf3166d2246904a8766d5d403dd23706f4892 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Fri, 22 Apr 2016 15:29:33 +0200 Subject: [PATCH 34/47] fix(panels): avoid deep copy problem for panel defaults Moves the defaults to be an instance property. This solves the problem of lodash defaults function not doing a deep copy. The symptom of this is that new deep properties for a panel are assigned to every panel of the same type by the defaults function in the constructor. --- public/app/plugins/panel/graph/module.ts | 166 +++++++++--------- public/app/plugins/panel/pluginlist/module.ts | 10 +- public/app/plugins/panel/singlestat/module.ts | 78 ++++---- public/app/plugins/panel/table/module.ts | 56 +++--- public/app/plugins/panel/text/module.ts | 14 +- 5 files changed, 161 insertions(+), 163 deletions(-) diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index 1fcd44204f7..4608de804aa 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -13,85 +13,6 @@ import TimeSeries from 'app/core/time_series2'; import * as fileExport from 'app/core/utils/file_export'; import {MetricsPanelCtrl} from 'app/plugins/sdk'; -var panelDefaults = { - // datasource name, null = default datasource - datasource: null, - // sets client side (flot) or native graphite png renderer (png) - renderer: 'flot', - yaxes: [ - { - label: null, - show: true, - logBase: 1, - min: null, - max: null, - format: 'short' - }, - { - label: null, - show: true, - logBase: 1, - min: null, - max: null, - format: 'short' - } - ], - xaxis: { - show: true - }, - grid : { - threshold1: null, - threshold2: null, - threshold1Color: 'rgba(216, 200, 27, 0.27)', - threshold2Color: 'rgba(234, 112, 112, 0.22)' - }, - // show/hide lines - lines : true, - // fill factor - fill : 1, - // line width in pixels - linewidth : 2, - // show hide points - points : false, - // point radius in pixels - pointradius : 5, - // show hide bars - bars : false, - // enable/disable stacking - stack : false, - // stack percentage mode - percentage : false, - // legend options - legend: { - show: true, // disable/enable legend - values: false, // disable/enable legend values - min: false, - max: false, - current: false, - total: false, - avg: false - }, - // how null points should be handled - nullPointMode : 'connected', - // staircase line mode - steppedLine: false, - // tooltip options - tooltip : { - value_type: 'cumulative', - shared: true, - msResolution: false, - }, - // time overrides - timeFrom: null, - timeShift: null, - // metric queries - targets: [{}], - // series color overrides - aliasColors: {}, - // other style overrides - seriesOverrides: [], -}; - class GraphCtrl extends MetricsPanelCtrl { static template = template; @@ -105,14 +26,93 @@ class GraphCtrl extends MetricsPanelCtrl { datapointsWarning: boolean; colors: any = []; + panelDefaults = { + // datasource name, null = default datasource + datasource: null, + // sets client side (flot) or native graphite png renderer (png) + renderer: 'flot', + yaxes: [ + { + label: null, + show: true, + logBase: 1, + min: null, + max: null, + format: 'short' + }, + { + label: null, + show: true, + logBase: 1, + min: null, + max: null, + format: 'short' + } + ], + xaxis: { + show: true + }, + grid : { + threshold1: null, + threshold2: null, + threshold1Color: 'rgba(216, 200, 27, 0.27)', + threshold2Color: 'rgba(234, 112, 112, 0.22)' + }, + // show/hide lines + lines : true, + // fill factor + fill : 1, + // line width in pixels + linewidth : 2, + // show hide points + points : false, + // point radius in pixels + pointradius : 5, + // show hide bars + bars : false, + // enable/disable stacking + stack : false, + // stack percentage mode + percentage : false, + // legend options + legend: { + show: true, // disable/enable legend + values: false, // disable/enable legend values + min: false, + max: false, + current: false, + total: false, + avg: false + }, + // how null points should be handled + nullPointMode : 'connected', + // staircase line mode + steppedLine: false, + // tooltip options + tooltip : { + value_type: 'cumulative', + shared: true, + msResolution: false, + }, + // time overrides + timeFrom: null, + timeShift: null, + // metric queries + targets: [{}], + // series color overrides + aliasColors: {}, + // other style overrides + seriesOverrides: [], + }; + /** @ngInject */ constructor($scope, $injector, private annotationsSrv) { super($scope, $injector); - _.defaults(this.panel, angular.copy(panelDefaults)); - _.defaults(this.panel.tooltip, panelDefaults.tooltip); - _.defaults(this.panel.grid, panelDefaults.grid); - _.defaults(this.panel.legend, panelDefaults.legend); + _.defaults(this.panel, angular.copy(this.panelDefaults)); + _.defaults(this.panel.tooltip, this.panelDefaults.tooltip); + _.defaults(this.panel.grid, this.panelDefaults.grid); + _.defaults(this.panel.legend, this.panelDefaults.legend); this.colors = $scope.$root.colors; diff --git a/public/app/plugins/panel/pluginlist/module.ts b/public/app/plugins/panel/pluginlist/module.ts index 8ec86efd9c2..5ef1fb6fb0c 100644 --- a/public/app/plugins/panel/pluginlist/module.ts +++ b/public/app/plugins/panel/pluginlist/module.ts @@ -4,20 +4,20 @@ import _ from 'lodash'; import config from 'app/core/config'; import {PanelCtrl} from '../../../features/panel/panel_ctrl'; -// Set and populate defaults -var panelDefaults = { -}; - class PluginListCtrl extends PanelCtrl { static templateUrl = 'module.html'; pluginList: any[]; viewModel: any; + // Set and populate defaults + panelDefaults = { + }; + /** @ngInject */ constructor($scope, $injector, private backendSrv, private $location) { super($scope, $injector); - _.defaults(this.panel, panelDefaults); + _.defaults(this.panel, this.panelDefaults); this.events.on('init-edit-mode', this.onInitEditMode.bind(this)); this.pluginList = []; diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index 6d60a70269e..0e28aa8baba 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -11,44 +11,6 @@ import config from 'app/core/config'; import TimeSeries from 'app/core/time_series2'; import {MetricsPanelCtrl} from 'app/plugins/sdk'; -// Set and populate defaults -var panelDefaults = { - links: [], - datasource: null, - maxDataPoints: 100, - interval: null, - targets: [{}], - cacheTimeout: null, - format: 'none', - prefix: '', - postfix: '', - nullText: null, - valueMaps: [ - { value: 'null', op: '=', text: 'N/A' } - ], - nullPointMode: 'connected', - valueName: 'avg', - prefixFontSize: '50%', - valueFontSize: '80%', - postfixFontSize: '50%', - thresholds: '', - colorBackground: false, - colorValue: false, - colors: ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"], - sparkline: { - show: false, - full: false, - lineColor: 'rgb(31, 120, 193)', - fillColor: 'rgba(31, 118, 189, 0.18)', - }, - gauge: { - show: false, - minValue: 0, - maxValue: 100, - thresholdLabels: true - } -}; - class SingleStatCtrl extends MetricsPanelCtrl { static templateUrl = 'module.html'; @@ -57,10 +19,48 @@ class SingleStatCtrl extends MetricsPanelCtrl { fontSizes: any[]; unitFormats: any[]; + // Set and populate defaults + panelDefaults = { + links: [], + datasource: null, + maxDataPoints: 100, + interval: null, + targets: [{}], + cacheTimeout: null, + format: 'none', + prefix: '', + postfix: '', + nullText: null, + valueMaps: [ + { value: 'null', op: '=', text: 'N/A' } + ], + nullPointMode: 'connected', + valueName: 'avg', + prefixFontSize: '50%', + valueFontSize: '80%', + postfixFontSize: '50%', + thresholds: '', + colorBackground: false, + colorValue: false, + colors: ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"], + sparkline: { + show: false, + full: false, + lineColor: 'rgb(31, 120, 193)', + fillColor: 'rgba(31, 118, 189, 0.18)', + }, + gauge: { + show: false, + minValue: 0, + maxValue: 100, + thresholdLabels: true + } + }; + /** @ngInject */ constructor($scope, $injector, private $location, private linkSrv) { super($scope, $injector); - _.defaults(this.panel, panelDefaults); + _.defaults(this.panel, this.panelDefaults); this.events.on('data-received', this.onDataReceived.bind(this)); this.events.on('data-error', this.onDataError.bind(this)); diff --git a/public/app/plugins/panel/table/module.ts b/public/app/plugins/panel/table/module.ts index 7a2973cc390..837cf0df916 100644 --- a/public/app/plugins/panel/table/module.ts +++ b/public/app/plugins/panel/table/module.ts @@ -10,33 +10,6 @@ import {transformDataToTable} from './transformers'; import {tablePanelEditor} from './editor'; import {TableRenderer} from './renderer'; -var panelDefaults = { - targets: [{}], - transform: 'timeseries_to_columns', - pageSize: null, - showHeader: true, - styles: [ - { - type: 'date', - pattern: 'Time', - dateFormat: 'YYYY-MM-DD HH:mm:ss', - }, - { - unit: 'short', - type: 'number', - decimals: 2, - colors: ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"], - colorMode: null, - pattern: '/.*/', - thresholds: [], - } - ], - columns: [], - scroll: true, - fontSize: '100%', - sort: {col: 0, desc: true}, -}; - class TablePanelCtrl extends MetricsPanelCtrl { static templateUrl = 'module.html'; @@ -44,6 +17,33 @@ class TablePanelCtrl extends MetricsPanelCtrl { dataRaw: any; table: any; + panelDefaults = { + targets: [{}], + transform: 'timeseries_to_columns', + pageSize: null, + showHeader: true, + styles: [ + { + type: 'date', + pattern: 'Time', + dateFormat: 'YYYY-MM-DD HH:mm:ss', + }, + { + unit: 'short', + type: 'number', + decimals: 2, + colors: ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"], + colorMode: null, + pattern: '/.*/', + thresholds: [], + } + ], + columns: [], + scroll: true, + fontSize: '100%', + sort: {col: 0, desc: true}, + }; + /** @ngInject */ constructor($scope, $injector, private annotationsSrv) { super($scope, $injector); @@ -56,7 +56,7 @@ class TablePanelCtrl extends MetricsPanelCtrl { delete this.panel.fields; } - _.defaults(this.panel, panelDefaults); + _.defaults(this.panel, this.panelDefaults); this.events.on('data-received', this.onDataReceived.bind(this)); this.events.on('data-error', this.onDataError.bind(this)); diff --git a/public/app/plugins/panel/text/module.ts b/public/app/plugins/panel/text/module.ts index 42657788c0a..7228f220d46 100644 --- a/public/app/plugins/panel/text/module.ts +++ b/public/app/plugins/panel/text/module.ts @@ -3,23 +3,21 @@ import _ from 'lodash'; import {PanelCtrl} from 'app/plugins/sdk'; - // Set and populate defaults -var panelDefaults = { - mode : "markdown", // 'html', 'markdown', 'text' - content : "# title", -}; - export class TextPanelCtrl extends PanelCtrl { static templateUrl = `public/app/plugins/panel/text/module.html`; remarkable: any; content: string; - + // Set and populate defaults + panelDefaults = { + mode : "markdown", // 'html', 'markdown', 'text' + content : "# title", + }; /** @ngInject */ constructor($scope, $injector, private templateSrv, private $sce) { super($scope, $injector); - _.defaults(this.panel, panelDefaults); + _.defaults(this.panel, this.panelDefaults); this.events.on('init-edit-mode', this.onInitEditMode.bind(this)); this.events.on('refresh', this.onRender.bind(this)); From 9917de09afe78d77b4641c67d272c43f2f4a0ba0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 22 Apr 2016 15:49:51 +0200 Subject: [PATCH 35/47] fix(graph): removed angular copy that was no longer needed --- public/app/plugins/panel/graph/module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index 4608de804aa..a11c537b178 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -109,7 +109,7 @@ class GraphCtrl extends MetricsPanelCtrl { constructor($scope, $injector, private annotationsSrv) { super($scope, $injector); - _.defaults(this.panel, angular.copy(this.panelDefaults)); + _.defaults(this.panel, this.panelDefaults); _.defaults(this.panel.tooltip, this.panelDefaults.tooltip); _.defaults(this.panel.grid, this.panelDefaults.grid); _.defaults(this.panel.legend, this.panelDefaults.legend); From 349f6b51a5d5417e3179af0b590df83a85a60a27 Mon Sep 17 00:00:00 2001 From: Matt Toback Date: Fri, 22 Apr 2016 11:32:19 -0400 Subject: [PATCH 36/47] Updated link color in text panel to be -link-color --- public/sass/components/_panel_text.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/public/sass/components/_panel_text.scss b/public/sass/components/_panel_text.scss index 9de31bab4c8..6e0301cecc5 100644 --- a/public/sass/components/_panel_text.scss +++ b/public/sass/components/_panel_text.scss @@ -4,4 +4,5 @@ margin: 0 0 $spacer $spacer * 1.5; } li {line-height: 2;} + a { color: $external-link-color; } } From 25b0773a947939708b2d7219cde684745e4b1abb Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Fri, 22 Apr 2016 12:12:36 -0400 Subject: [PATCH 37/47] fix broken link to grafana.net, standardize on https://grafana.net for links --- docs/sources/plugins/index.md | 2 +- public/app/features/plugins/partials/update_instructions.html | 2 +- public/app/plugins/datasource/influxdb/README.md | 4 ++-- public/app/plugins/panel/pluginlist/module.html | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/sources/plugins/index.md b/docs/sources/plugins/index.md index 08654bfb946..bb5c8062223 100644 --- a/docs/sources/plugins/index.md +++ b/docs/sources/plugins/index.md @@ -15,7 +15,7 @@ Grafana already have a strong community of contributors and plugin developers. By making it easier to develop and install plugins we hope that the community can grow even stronger and develop new plugins that we would never think about. -You can discover available plugins on [Grafana.net](http://grafana.net) +You can discover available plugins on [Grafana.net](https://grafana.net) diff --git a/public/app/features/plugins/partials/update_instructions.html b/public/app/features/plugins/partials/update_instructions.html index 55455e24af7..9648be53c86 100644 --- a/public/app/features/plugins/partials/update_instructions.html +++ b/public/app/features/plugins/partials/update_instructions.html @@ -14,7 +14,7 @@

Type the following on the command line to update {{plugin.name}}.

grafana-cli plugins update {{plugin.id}}
- Check out {{plugin.name}} on
Grafana.net for README and changelog. If you do not have access to the command line, ask your Grafana administator. + Check out {{plugin.name}} on Grafana.net for README and changelog. If you do not have access to the command line, ask your Grafana administator.

Pro tip: To update all plugins at once, type grafana-cli plugins update-all on the command line.

diff --git a/public/app/plugins/datasource/influxdb/README.md b/public/app/plugins/datasource/influxdb/README.md index 45eaa51eb0f..d8d98cf5c6b 100644 --- a/public/app/plugins/datasource/influxdb/README.md +++ b/public/app/plugins/datasource/influxdb/README.md @@ -6,8 +6,8 @@ There are currently two separate datasources for InfluxDB in Grafana: InfluxDB 0 This is the plugin for InfluxDB 0.9. It is rapidly evolving and we continue to track its API. -InfluxDB 0.8 is no longer maintained by InfluxDB Inc, but we provide support as a convenience to existing users. You can find it [here](https://www.grafana.net/plugins/grafana-influxdb-08-datasource). +InfluxDB 0.8 is no longer maintained by InfluxDB Inc, but we provide support as a convenience to existing users. You can find it [here](https://grafana.net/plugins/grafana-influxdb-08-datasource). Read more about InfluxDB here: -[http://docs.grafana.org/datasources/influxdb/](http://docs.grafana.org/datasources/influxdb/) \ No newline at end of file +[http://docs.grafana.org/datasources/influxdb/](http://docs.grafana.org/datasources/influxdb/) diff --git a/public/app/plugins/panel/pluginlist/module.html b/public/app/plugins/panel/pluginlist/module.html index c73da35b391..8e74f173a07 100644 --- a/public/app/plugins/panel/pluginlist/module.html +++ b/public/app/plugins/panel/pluginlist/module.html @@ -22,7 +22,7 @@
From 70acfb2cfd942d75eb0fc75ef610d909a8d26c3e Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 21 Apr 2016 11:15:51 +0200 Subject: [PATCH 38/47] fix(cli): adds better help text. The zip lib is throwing panics sometimes when the response is malformed. The cli will now try to download the zip file up to three times before aborting. The cli gives a better error message and informes the user about retrying. closes #4651 --- pkg/cmd/grafana-cli/commands/install_command.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/grafana-cli/commands/install_command.go b/pkg/cmd/grafana-cli/commands/install_command.go index addcf9a8b7e..1ed3a8a3fe8 100644 --- a/pkg/cmd/grafana-cli/commands/install_command.go +++ b/pkg/cmd/grafana-cli/commands/install_command.go @@ -127,9 +127,14 @@ func downloadFile(pluginName, filePath, url string) (err error) { if r := recover(); r != nil { retryCount++ if retryCount < 3 { - fmt.Printf("\nFailed downloading. Will retry once.\n%v\n", r) + + fmt.Println("Failed downloading. Will retry once.") downloadFile(pluginName, filePath, url) } else { + failure := fmt.Sprintf("%v", r) + if failure == "runtime error: makeslice: len out of range" { + log.Errorf("Failed to extract zipped HTTP response. Please try again.\n") + } panic(r) } } From 0855f514363146a2623a3081fd337982c5c940a5 Mon Sep 17 00:00:00 2001 From: bergquist Date: Sat, 23 Apr 2016 10:03:00 +0200 Subject: [PATCH 39/47] feat(cli): improves defer error handling --- pkg/cmd/grafana-cli/commands/install_command.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/grafana-cli/commands/install_command.go b/pkg/cmd/grafana-cli/commands/install_command.go index 1ed3a8a3fe8..a07b2dcae38 100644 --- a/pkg/cmd/grafana-cli/commands/install_command.go +++ b/pkg/cmd/grafana-cli/commands/install_command.go @@ -127,15 +127,15 @@ func downloadFile(pluginName, filePath, url string) (err error) { if r := recover(); r != nil { retryCount++ if retryCount < 3 { - fmt.Println("Failed downloading. Will retry once.") - downloadFile(pluginName, filePath, url) + err = downloadFile(pluginName, filePath, url) } else { failure := fmt.Sprintf("%v", r) if failure == "runtime error: makeslice: len out of range" { - log.Errorf("Failed to extract zipped HTTP response. Please try again.\n") + err = fmt.Errorf("Failed to extract zipped HTTP response. Please try again.\n") + } else { + panic(r) } - panic(r) } } }() From 07b13e24fa43e7c171046f6d77ab13f053af3a3c Mon Sep 17 00:00:00 2001 From: bergquist Date: Sat, 23 Apr 2016 14:31:24 +0200 Subject: [PATCH 40/47] style(cli): add some color to error messages --- pkg/cmd/grafana-cli/commands/commands.go | 3 ++- pkg/cmd/grafana-cli/commands/install_command.go | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/grafana-cli/commands/commands.go b/pkg/cmd/grafana-cli/commands/commands.go index b3821a47844..ec454078f9b 100644 --- a/pkg/cmd/grafana-cli/commands/commands.go +++ b/pkg/cmd/grafana-cli/commands/commands.go @@ -4,6 +4,7 @@ import ( "os" "github.com/codegangsta/cli" + "github.com/fatih/color" "github.com/grafana/grafana/pkg/cmd/grafana-cli/log" ) @@ -12,7 +13,7 @@ func runCommand(command func(commandLine CommandLine) error) func(context *cli.C cmd := &contextCommandLine{context} if err := command(cmd); err != nil { - log.Error("\nError: ") + log.Errorf("\n%s: ", color.RedString("Error")) log.Errorf("%s\n\n", err) cmd.ShowHelp() diff --git a/pkg/cmd/grafana-cli/commands/install_command.go b/pkg/cmd/grafana-cli/commands/install_command.go index a07b2dcae38..eb5973d07be 100644 --- a/pkg/cmd/grafana-cli/commands/install_command.go +++ b/pkg/cmd/grafana-cli/commands/install_command.go @@ -132,7 +132,7 @@ func downloadFile(pluginName, filePath, url string) (err error) { } else { failure := fmt.Sprintf("%v", r) if failure == "runtime error: makeslice: len out of range" { - err = fmt.Errorf("Failed to extract zipped HTTP response. Please try again.\n") + err = fmt.Errorf("Corrupt http response from source. Please try again.\n") } else { panic(r) } From ee0c4cd1945b646ebf7671eaecb8f1acee358cb4 Mon Sep 17 00:00:00 2001 From: bergquist Date: Sat, 23 Apr 2016 14:37:26 +0200 Subject: [PATCH 41/47] docs(changelog): add note about cli issue closes #4651 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba8e6242243..9d4ee020dce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ * **Dashlist**: Fixed issue dashboard list panel and caching tags, fixes [#4768](https://github.com/grafana/grafana/issues/4768) * **Graph**: Fixed issue with unneeded scrollbar in legend for Firefox, fixes [#4760](https://github.com/grafana/grafana/issues/4760) * **Table panel**: Fixed issue table panel formating string array properties, fixes [#4791](https://github.com/grafana/grafana/issues/4791) +* **grafana-cli**: Improve error message when failing to install plugins due to corrupt response, fixes [#4651](https://github.com/grafana/grafana/issues/4651) # 3.0.0-beta5 (2016-04-15) From 21d2a51cd335da768af4d12a77842ac2e48bf852 Mon Sep 17 00:00:00 2001 From: Tyler Mitchell Date: Sat, 23 Apr 2016 22:00:42 -0700 Subject: [PATCH 42/47] typo tabs != tags --- docs/sources/http_api/dashboard.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/http_api/dashboard.md b/docs/sources/http_api/dashboard.md index 68f6ab022cc..ead63c722aa 100644 --- a/docs/sources/http_api/dashboard.md +++ b/docs/sources/http_api/dashboard.md @@ -191,7 +191,7 @@ Will return the home dashboard. `GET /api/dashboards/tags` -Get all tabs of dashboards +Get all tags of dashboards **Example Request**: From 00827ce921352807315e05ef664d3cba77c29a69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 24 Apr 2016 12:50:33 +0200 Subject: [PATCH 43/47] fix(): let binding cycle complete before adding panel to dom --- public/app/core/directives/plugin_component.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/public/app/core/directives/plugin_component.ts b/public/app/core/directives/plugin_component.ts index 3e6383cc5c6..6708c0315f3 100644 --- a/public/app/core/directives/plugin_component.ts +++ b/public/app/core/directives/plugin_component.ts @@ -206,9 +206,12 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ }); $compile(child)(scope); - elem.empty(); - elem.append(child); + + // let a binding digest cycle complete before adding to dom + setTimeout(function() { + elem.append(child); + }); } function registerPluginComponent(scope, elem, attrs, componentInfo) { From 711992c8e2e811abd450cb665008bb0a1b3e29a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 25 Apr 2016 09:26:47 +0200 Subject: [PATCH 44/47] fix(influxdb): minor fix for duplicated db param to proxied influxdb call, fixes #4703 --- pkg/api/dataproxy.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/api/dataproxy.go b/pkg/api/dataproxy.go index eb88045cd51..b00ef595161 100644 --- a/pkg/api/dataproxy.go +++ b/pkg/api/dataproxy.go @@ -41,7 +41,6 @@ func NewReverseProxy(ds *m.DataSource, proxyPath string, targetUrl *url.URL) *ht req.URL.RawQuery = reqQueryVals.Encode() } else if ds.Type == m.DS_INFLUXDB { req.URL.Path = util.JoinUrlFragments(targetUrl.Path, proxyPath) - reqQueryVals.Add("db", ds.Database) req.URL.RawQuery = reqQueryVals.Encode() if !ds.BasicAuth { req.Header.Del("Authorization") From 97656d65ec2c9a29197f37750aba392c8d6dd4be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 25 Apr 2016 11:22:13 +0200 Subject: [PATCH 45/47] db(dashboard): minor fix for dashboard delete --- pkg/services/sqlstore/dashboard.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index a64094cb65e..ef36fd75ab6 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -216,7 +216,7 @@ func GetDashboardTags(query *m.GetDashboardTagsQuery) error { func DeleteDashboard(cmd *m.DeleteDashboardCommand) error { return inTransaction2(func(sess *session) error { dashboard := m.Dashboard{Slug: cmd.Slug, OrgId: cmd.OrgId} - has, err := x.Get(&dashboard) + has, err := sess.Get(&dashboard) if err != nil { return err } else if has == false { From 04a79175bfdfa0fa705eb3c0852e599d9108d91d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 25 Apr 2016 14:00:49 +0200 Subject: [PATCH 46/47] fix(plugins): only add app nav link if it has any pages/dashboards with role matching current user, fixes #4784 --- pkg/api/index.go | 9 ++++++++- pkg/models/org_user.go | 22 ++++++++++++++++++++++ pkg/plugins/models.go | 24 +++++++++++++++--------- 3 files changed, 45 insertions(+), 10 deletions(-) diff --git a/pkg/api/index.go b/pkg/api/index.go index 575ea35cfaf..53538fd2775 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -103,6 +103,10 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { } for _, include := range plugin.Includes { + if !c.HasUserRole(include.Role) { + continue + } + if include.Type == "page" && include.AddToNav { link := &dtos.NavLink{ Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/page/" + include.Slug, @@ -110,6 +114,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { } appLink.Children = append(appLink.Children, link) } + if include.Type == "dashboard" && include.AddToNav { link := &dtos.NavLink{ Url: setting.AppSubUrl + "/dashboard/db/" + include.Slug, @@ -124,7 +129,9 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { appLink.Children = append(appLink.Children, &dtos.NavLink{Text: "Plugin Config", Icon: "fa fa-cog", Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/edit"}) } - data.MainNavLinks = append(data.MainNavLinks, appLink) + if len(appLink.Children) > 0 { + data.MainNavLinks = append(data.MainNavLinks, appLink) + } } } diff --git a/pkg/models/org_user.go b/pkg/models/org_user.go index 48d17deb9db..d7a918751ef 100644 --- a/pkg/models/org_user.go +++ b/pkg/models/org_user.go @@ -1,7 +1,9 @@ package models import ( + "encoding/json" "errors" + "fmt" "time" ) @@ -37,6 +39,26 @@ func (r RoleType) Includes(other RoleType) bool { return r == other } +func (r *RoleType) UnmarshalJSON(data []byte) error { + var str string + err := json.Unmarshal(data, &str) + if err != nil { + return err + } + + *r = RoleType(str) + + if (*r).IsValid() == false { + if (*r) != "" { + return errors.New(fmt.Sprintf("JSON validation error: invalid role value: %s", *r)) + } + + *r = ROLE_VIEWER + } + + return nil +} + type OrgUser struct { Id int64 OrgId int64 diff --git a/pkg/plugins/models.go b/pkg/plugins/models.go index 68268239c51..aa381abacc7 100644 --- a/pkg/plugins/models.go +++ b/pkg/plugins/models.go @@ -7,7 +7,7 @@ import ( "strings" "github.com/grafana/grafana/pkg/log" - "github.com/grafana/grafana/pkg/models" + m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" ) @@ -69,6 +69,12 @@ func (pb *PluginBase) registerPlugin(pluginDir string) error { pb.Dependencies.GrafanaVersion = "*" } + for _, include := range pb.Includes { + if include.Role == "" { + include.Role = m.RoleType(m.ROLE_VIEWER) + } + } + pb.PluginDir = pluginDir Plugins[pb.Id] = pb return nil @@ -80,14 +86,14 @@ type PluginDependencies struct { } type PluginInclude struct { - Name string `json:"name"` - Path string `json:"path"` - Type string `json:"type"` - Component string `json:"component"` - Role models.RoleType `json:"role"` - AddToNav bool `json:"addToNav"` - DefaultNav bool `json:"defaultNav"` - Slug string `json:"slug"` + Name string `json:"name"` + Path string `json:"path"` + Type string `json:"type"` + Component string `json:"component"` + Role m.RoleType `json:"role"` + AddToNav bool `json:"addToNav"` + DefaultNav bool `json:"defaultNav"` + Slug string `json:"slug"` Id string `json:"-"` } From 4d802df040921779434ba223bece0a94a56ae47d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 25 Apr 2016 14:22:15 +0200 Subject: [PATCH 47/47] fix(influxdb): fixed issue with using multi value template vars in influxdb measurement clause, fixes #4797 --- public/app/plugins/datasource/influxdb/influx_query.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/app/plugins/datasource/influxdb/influx_query.ts b/public/app/plugins/datasource/influxdb/influx_query.ts index 8143e187b76..68392348f1a 100644 --- a/public/app/plugins/datasource/influxdb/influx_query.ts +++ b/public/app/plugins/datasource/influxdb/influx_query.ts @@ -168,6 +168,8 @@ export default class InfluxQuery { if (!measurement.match('^/.*/')) { measurement = '"' + measurement+ '"'; + } else { + measurement = this.templateSrv.replace(measurement, this.scopedVars, 'regex'); } if (policy !== 'default') {