diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b8239bd437..ba8e6242243 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +# 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) +* **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) ### Bug fixes 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": { 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"}, 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/cmd/grafana-cli/commands/install_command.go b/pkg/cmd/grafana-cli/commands/install_command.go index 81e7a15233b..addcf9a8b7e 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) @@ -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) + log.Errorf("Failed to extract file: %v", err) } - defer src.Close() io.Copy(dst, src) + dst.Close() + src.Close() } } 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()) } 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() { 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 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(); } 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/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/app/plugins/datasource/elasticsearch/img/logo_large.png b/public/app/plugins/datasource/elasticsearch/img/logo_large.png index 429dedac1a7..b76d01e5d74 100644 Binary files a/public/app/plugins/datasource/elasticsearch/img/logo_large.png and b/public/app/plugins/datasource/elasticsearch/img/logo_large.png differ 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, 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 + "'"; } diff --git a/public/app/plugins/datasource/influxdb/response_parser.ts b/public/app/plugins/datasource/influxdb/response_parser.ts index 2e33b398a88..23173674361 100644 --- a/public/app/plugins/datasource/influxdb/response_parser.ts +++ b/public/app/plugins/datasource/influxdb/response_parser.ts @@ -12,17 +12,29 @@ 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 influxdb11format = query.toLowerCase().indexOf('show tag values') >= 0; + + var res = {}; + _.each(influxResults.series, serie => { + _.each(serie.values, value => { + if (_.isArray(value)) { + if (influxdb11format) { + addUnique(res, value[1] || value[0]); + } else { + addUnique(res, value[0]); + } } else { - return { text: value[0] }; + addUnique(res, value); } - } else { - return { text: value }; - } + }); + }); + + return _.map(res, value => { + return { text: value}; }); } } + +function addUnique(arr, value) { + arr[value] = 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", () => { 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]; 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; 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; 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; + } } } } 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; 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/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() { 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'); }); }); 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) + } + }); + }); + }); };