diff --git a/CHANGELOG.md b/CHANGELOG.md index c7763166f20..dcdc838115c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ * **Prometheus**: Prometheus annotation support, closes[#2883](https://github.com/grafana/grafana/pull/2883) * **Cli**: New cli tool for downloading and updating plugins * **Annotations**: Annotations can now contain links that can be clicked (you can navigate on to annotation popovers), closes [#1588](https://github.com/grafana/grafana/issues/1588) +* **Opentsdb**: Opentsdb 2.2 filters support, closes[#3077](https://github.com/grafana/grafana/issues/3077) ### Breaking changes * **Plugin API**: Both datasource and panel plugin api (and plugin.json schema) have been updated, requiring an update to plugins. See [plugin api](https://github.com/grafana/grafana/blob/master/public/app/plugins/plugin_api.md) for more info. @@ -21,6 +22,7 @@ * **Admin**: Admin can now have global overview of Grafana setup, closes [#3812](https://github.com/grafana/grafana/issues/3812) * **graph**: Right side legend height is now fixed at row height, closes [#1277](https://github.com/grafana/grafana/issues/1277) * **Table**: All content in table panel is now html escaped, closes [#3673](https://github.com/grafana/grafana/issues/3673) +* **graph**: Template variables can now be used in TimeShift and TimeFrom, closes[#1960](https://github.com/grafana/grafana/issues/1960) ### Bug fixes * **Playlist**: Fix for memory leak when running a playlist, closes [#3794](https://github.com/grafana/grafana/pull/3794) diff --git a/docker/blocks/elastic/elasticsearch.yml b/docker/blocks/elastic/elasticsearch.yml new file mode 100644 index 00000000000..c57b2c12908 --- /dev/null +++ b/docker/blocks/elastic/elasticsearch.yml @@ -0,0 +1,2 @@ +script.inline: on +script.indexed: on diff --git a/docker/blocks/elastic/elasticsearch/config/.placeholder b/docker/blocks/elastic/elasticsearch/config/.placeholder deleted file mode 100644 index 9ad266259c2..00000000000 --- a/docker/blocks/elastic/elasticsearch/config/.placeholder +++ /dev/null @@ -1 +0,0 @@ -Ensure the existence of the parent folder. diff --git a/docker/blocks/elastic/fig b/docker/blocks/elastic/fig index 498402ac7b0..357352cabaa 100644 --- a/docker/blocks/elastic/fig +++ b/docker/blocks/elastic/fig @@ -4,3 +4,5 @@ elasticsearch: ports: - "9200:9200" - "9300:9300" + volumes: + - ./blocks/elastic/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml diff --git a/docs/sources/datasources/opentsdb.md b/docs/sources/datasources/opentsdb.md index 43fcda643ee..757ddcefab5 100644 --- a/docs/sources/datasources/opentsdb.md +++ b/docs/sources/datasources/opentsdb.md @@ -23,6 +23,7 @@ Name | The data source name, important that this is the same as in Grafana v1.x Default | Default data source means that it will be pre-selected for new panels. Url | The http protocol, ip and port of you opentsdb server (default port is usually 4242) Access | Proxy = access via Grafana backend, Direct = access directory from browser. +Version | Version = opentsdb version, either <=2.1 or 2.2 ## Query editor Open a graph in edit mode by click the title. diff --git a/pkg/api/api.go b/pkg/api/api.go index 90d613e46e0..ed029a5171a 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -43,8 +43,9 @@ func Register(r *macaron.Macaron) { r.Get("/admin/orgs/edit/:id", reqGrafanaAdmin, Index) r.Get("/admin/stats", reqGrafanaAdmin, Index) - r.Get("/apps", reqSignedIn, Index) - r.Get("/apps/edit/*", reqSignedIn, Index) + r.Get("/plugins", reqSignedIn, Index) + r.Get("/plugins/:id/edit", reqSignedIn, Index) + r.Get("/plugins/:id/page/:page", reqSignedIn, Index) r.Get("/dashboard/*", reqSignedIn, Index) r.Get("/dashboard-solo/*", reqSignedIn, Index) diff --git a/pkg/api/index.go b/pkg/api/index.go index df752109530..a5199fa79ae 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -85,13 +85,13 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { if plugin.Pinned { pageLink := &dtos.NavLink{ Text: plugin.Name, - Url: setting.AppSubUrl + "/apps/" + plugin.Id + "/edit", + Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/edit", Img: plugin.Info.Logos.Small, } for _, page := range plugin.Pages { pageLink.Children = append(pageLink.Children, &dtos.NavLink{ - Url: setting.AppSubUrl + "/apps/" + plugin.Id + "/page/" + page.Slug, + Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/page/" + page.Slug, Text: page.Name, }) } diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index e3db2d7f537..8dae7247a39 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -77,7 +77,7 @@ func NewEngine() { log.Fatal(3, "Sqlstore: Fail to connect to database: %v", err) } - err = SetEngine(x, true) + err = SetEngine(x, setting.Env == setting.DEV) if err != nil { log.Fatal(3, "fail to initialize orm engine: %v", err) @@ -105,14 +105,6 @@ func SetEngine(engine *xorm.Engine, enableLog bool) (err error) { return fmt.Errorf("sqlstore.init(fail to create xorm.log): %v", err) } x.Logger = xorm.NewSimpleLogger(f) - - if setting.Env == setting.DEV { - x.ShowSQL = false - x.ShowInfo = false - x.ShowDebug = false - x.ShowErr = true - x.ShowWarn = true - } } return nil diff --git a/public/app/core/directives/ng_model_on_blur.js b/public/app/core/directives/ng_model_on_blur.js index 1e3ebc38a25..6f4a55b53f0 100644 --- a/public/app/core/directives/ng_model_on_blur.js +++ b/public/app/core/directives/ng_model_on_blur.js @@ -47,6 +47,9 @@ function (coreModule, kbn, rangeUtil) { if (ctrl.$isEmpty(modelValue)) { return true; } + if (viewValue.indexOf('$') === 0) { + return true; // allow template variable + } var info = rangeUtil.describeTextRange(viewValue); return info.invalid !== true; }; diff --git a/public/app/core/utils/file_export.ts b/public/app/core/utils/file_export.ts index ad203f58495..944b6ae8a80 100644 --- a/public/app/core/utils/file_export.ts +++ b/public/app/core/utils/file_export.ts @@ -14,6 +14,41 @@ export function exportSeriesListToCsv(seriesList) { saveSaveBlob(text, 'grafana_data_export.csv'); }; +export function exportSeriesListToCsvColumns(seriesList) { + var text = 'Time;'; + // add header + _.each(seriesList, function(series) { + text += series.alias + ';'; + }); + text = text.substring(0,text.length-1); + text += '\n'; + + // process data + var dataArr = [[]]; + var sIndex = 1; + _.each(seriesList, function(series) { + var cIndex = 0; + dataArr.push([]); + _.each(series.datapoints, function(dp) { + dataArr[0][cIndex] = new Date(dp[1]).toISOString(); + dataArr[sIndex][cIndex] = dp[0]; + cIndex++; + }); + sIndex++; + }); + + // make text + for (var i = 0; i < dataArr[0].length; i++) { + text += dataArr[0][i] + ';'; + for (var j = 1; j < dataArr.length; j++) { + text += dataArr[j][i] + ';'; + } + text = text.substring(0,text.length-1); + text += '\n'; + } + saveSaveBlob(text, 'grafana_data_export.csv'); +}; + export function exportTableDataToCsv(table) { var text = ''; // add header diff --git a/public/app/core/utils/kbn.js b/public/app/core/utils/kbn.js index 97f2f10f799..f6e4277523e 100644 --- a/public/app/core/utils/kbn.js +++ b/public/app/core/utils/kbn.js @@ -12,39 +12,66 @@ function($, _) { kbn.round_interval = function(interval) { switch (true) { - // 0.5s - case (interval <= 500): + // 0.3s + case (interval <= 300): return 100; // 0.1s - // 5s - case (interval <= 5000): + // 0.75s + case (interval <= 750): + return 500; // 0.5s + // 1.5s + case (interval <= 1500): return 1000; // 1s + // 3.5s + case (interval <= 3500): + return 2000; // 2s // 7.5s case (interval <= 7500): return 5000; // 5s - // 15s - case (interval <= 15000): + // 12.5s + case (interval <= 12500): return 10000; // 10s + // 17.5s + case (interval <= 17500): + return 15000; // 15s + // 25s + case (interval <= 25000): + return 20000; // 20s // 45s case (interval <= 45000): return 30000; // 30s - // 3m - case (interval <= 180000): + // 1.5m + case (interval <= 90000): return 60000; // 1m - // 9m + // 3.5m + case (interval <= 210000): + return 120000; // 2m + // 7.5m case (interval <= 450000): return 300000; // 5m - // 20m - case (interval <= 1200000): + // 12.5m + case (interval <= 750000): return 600000; // 10m + // 12.5m + case (interval <= 1050000): + return 900000; // 15m + // 25m + case (interval <= 1500000): + return 1200000; // 20m // 45m case (interval <= 2700000): return 1800000; // 30m - // 2h - case (interval <= 7200000): + // 1.5h + case (interval <= 5400000): return 3600000; // 1h - // 6h - case (interval <= 21600000): + // 2.5h + case (interval <= 9000000): + return 7200000; // 2h + // 4.5h + case (interval <= 16200000): return 10800000; // 3h + // 9h + case (interval <= 32400000): + return 21600000; // 6h // 24h case (interval <= 86400000): return 43200000; // 12h diff --git a/public/app/features/annotations/editor_ctrl.js b/public/app/features/annotations/editor_ctrl.js index d9731686a4f..8d3887ee757 100644 --- a/public/app/features/annotations/editor_ctrl.js +++ b/public/app/features/annotations/editor_ctrl.js @@ -12,10 +12,7 @@ function (angular, _, $) { var annotationDefaults = { name: '', datasource: null, - showLine: true, - iconColor: '#C0C6BE', - lineColor: 'rgba(255, 96, 96, 0.592157)', - iconSize: 13, + iconColor: 'rgba(255, 96, 96, 1)', enable: true }; diff --git a/public/app/features/annotations/partials/editor.html b/public/app/features/annotations/partials/editor.html index c54d33fe939..702d8921bef 100644 --- a/public/app/features/annotations/partials/editor.html +++ b/public/app/features/annotations/partials/editor.html @@ -76,8 +76,8 @@
+
diff --git a/public/app/features/dashboard/all.js b/public/app/features/dashboard/all.js index 073ca2ae1d9..d5cc13b4270 100644 --- a/public/app/features/dashboard/all.js +++ b/public/app/features/dashboard/all.js @@ -16,4 +16,5 @@ define([ './graphiteImportCtrl', './dynamicDashboardSrv', './importCtrl', + './impressionStore', ], function () {}); diff --git a/public/app/features/dashboard/dashboardLoaderSrv.js b/public/app/features/dashboard/dashboardLoaderSrv.js index f578a9d1075..f975c47800f 100644 --- a/public/app/features/dashboard/dashboardLoaderSrv.js +++ b/public/app/features/dashboard/dashboardLoaderSrv.js @@ -5,8 +5,9 @@ define([ 'jquery', 'app/core/utils/kbn', 'app/core/utils/datemath', + './impressionStore', ], -function (angular, moment, _, $, kbn, dateMath) { +function (angular, moment, _, $, kbn, dateMath, impressionStore) { 'use strict'; var module = angular.module('grafana.services'); @@ -24,19 +25,27 @@ function (angular, moment, _, $, kbn, dateMath) { }; this.loadDashboard = function(type, slug) { - if (type === 'script') { - return this._loadScriptedDashboard(slug); - } + var promise; - if (type === 'snapshot') { - return backendSrv.get('/api/snapshots/' + $routeParams.slug).catch(function() { + if (type === 'script') { + promise = this._loadScriptedDashboard(slug); + } else if (type === 'snapshot') { + promise = backendSrv.get('/api/snapshots/' + $routeParams.slug).catch(function() { return {meta:{isSnapshot: true, canSave: false, canEdit: false}, dashboard: {title: 'Snapshot not found'}}; }); + } else { + promise = backendSrv.getDashboard($routeParams.type, $routeParams.slug) + .catch(function() { + return self._dashboardLoadFailed("Not found"); + }); } - return backendSrv.getDashboard($routeParams.type, $routeParams.slug).catch(function() { - return self._dashboardLoadFailed("Not found"); + promise.then(function(result) { + impressionStore.impressions.addDashboardImpression(slug); + return result; }); + + return promise; }; this._loadScriptedDashboard = function(file) { diff --git a/public/app/features/dashboard/dashboardSrv.js b/public/app/features/dashboard/dashboardSrv.js index be404dd1ce1..01c5787481b 100644 --- a/public/app/features/dashboard/dashboardSrv.js +++ b/public/app/features/dashboard/dashboardSrv.js @@ -140,7 +140,11 @@ function (angular, $, _, moment) { }; p.isSubmenuFeaturesEnabled = function() { - return this.templating.list.length > 0 || this.annotations.list.length > 0 || this.links.length > 0; + var visableTemplates = _.filter(this.templating.list, function(template) { + return template.hideVariable === undefined || template.hideVariable === false; + }); + + return visableTemplates.length > 0 || this.annotations.list.length > 0 || this.links.length > 0; }; p.getPanelInfoById = function(panelId) { diff --git a/public/app/features/dashboard/impressionStore.ts b/public/app/features/dashboard/impressionStore.ts new file mode 100644 index 00000000000..61be1131cb6 --- /dev/null +++ b/public/app/features/dashboard/impressionStore.ts @@ -0,0 +1,41 @@ +/// + +import store from 'app/core/store'; +import _ from 'lodash'; + +export class ImpressionsStore { + constructor() {} + + addDashboardImpression(slug) { + var impressions = []; + if (store.exists("dashboard_impressions")) { + impressions = JSON.parse(store.get("dashboard_impressions")); + if (!_.isArray(impressions)) { + impressions = []; + } + } + + var exists = impressions.indexOf(slug); + if (exists >= 0) { + impressions.splice(exists, 1); + } + + impressions.unshift(slug); + + if (impressions.length > 20) { + impressions.shift(); + } + store.set("dashboard_impressions", JSON.stringify(impressions)); + } + + getDashboardOpened() { + var k = store.get("dashboard_impressions"); + return JSON.parse(k); + } +} + +var impressions = new ImpressionsStore(); + +export { + impressions +}; diff --git a/public/app/features/dashboard/submenu/submenu.html b/public/app/features/dashboard/submenu/submenu.html index eb8de17676c..21a9744b359 100644 --- a/public/app/features/dashboard/submenu/submenu.html +++ b/public/app/features/dashboard/submenu/submenu.html @@ -1,6 +1,6 @@ -
- Label - - +
+
+ Label + +
+
+ + +
+
Value Options
@@ -109,10 +115,16 @@ Auto interval steps How many times should the current time range be divided to calculate the value -
- +
+
+
+ + Auto interval min value The calculated value will not go below this threshold + + +
diff --git a/public/app/features/templating/templateValuesSrv.js b/public/app/features/templating/templateValuesSrv.js index 6029bdf899c..2760bb42ca0 100644 --- a/public/app/features/templating/templateValuesSrv.js +++ b/public/app/features/templating/templateValuesSrv.js @@ -60,7 +60,7 @@ function (angular, _, kbn) { variable.options.unshift({ text: 'auto', value: '$__auto_interval' }); } - var interval = kbn.calculateInterval(timeSrv.timeRange(), variable.auto_count); + var interval = kbn.calculateInterval(timeSrv.timeRange(), variable.auto_count, (variable.auto_min ? ">"+variable.auto_min : null)); templateSrv.setGrafanaVariable('$__auto_interval', interval); }; diff --git a/public/app/partials/inspector.html b/public/app/partials/inspector.html index 9caa38bb39f..9187e429613 100644 --- a/public/app/partials/inspector.html +++ b/public/app/partials/inspector.html @@ -18,8 +18,8 @@
-
Request details
- +
Request details
+
@@ -38,8 +38,8 @@
Url {{inspector.error.config.url}}
-
Request parameters
- +
Request parameters
+
{{param.key}} diff --git a/public/app/partials/metrics.html b/public/app/partials/metrics.html index f7b1672cd97..7297d415c25 100644 --- a/public/app/partials/metrics.html +++ b/public/app/partials/metrics.html @@ -37,7 +37,7 @@ -
+
+
diff --git a/public/app/plugins/datasource/opentsdb/query_ctrl.ts b/public/app/plugins/datasource/opentsdb/query_ctrl.ts index 43096b2ec36..60466a00ff5 100644 --- a/public/app/plugins/datasource/opentsdb/query_ctrl.ts +++ b/public/app/plugins/datasource/opentsdb/query_ctrl.ts @@ -8,6 +8,8 @@ export class OpenTsQueryCtrl extends QueryCtrl { static templateUrl = 'partials/query.editor.html'; aggregators: any; fillPolicies: any; + filterTypes: any; + tsdbVersion: any; aggregator: any; downsampleInterval: any; downsampleAggregator: any; @@ -17,6 +19,7 @@ export class OpenTsQueryCtrl extends QueryCtrl { suggestTagKeys: any; suggestTagValues: any; addTagMode: boolean; + addFilterMode: boolean; /** @ngInject **/ constructor($scope, $injector) { @@ -25,6 +28,9 @@ export class OpenTsQueryCtrl extends QueryCtrl { this.errors = this.validateTarget(); this.aggregators = ['avg', 'sum', 'min', 'max', 'dev', 'zimsum', 'mimmin', 'mimmax']; this.fillPolicies = ['none', 'nan', 'null', 'zero']; + this.filterTypes = ['wildcard','iliteral_or','not_iliteral_or','not_literal_or','iwildcard','literal_or','regexp']; + + this.tsdbVersion = this.datasource.tsdbVersion; if (!this.target.aggregator) { this.target.aggregator = 'sum'; @@ -38,8 +44,16 @@ export class OpenTsQueryCtrl extends QueryCtrl { this.target.downsampleFillPolicy = 'none'; } - this.datasource.getAggregators().then(function(aggs) { - this.aggregators = aggs; + this.datasource.getAggregators().then((aggs) => { + if (aggs.length !== 0) { + this.aggregators = aggs; + } + }); + + this.datasource.getFilterTypes().then((filterTypes) => { + if (filterTypes.length !== 0) { + this.filterTypes = filterTypes; + } }); // needs to be defined here as it is called from typeahead @@ -70,6 +84,11 @@ export class OpenTsQueryCtrl extends QueryCtrl { } addTag() { + + if (this.target.filters && this.target.filters.length > 0) { + this.errors.tags = "Please remove filters to use tags, tags and filters are mutually exclusive."; + } + if (!this.addTagMode) { this.addTagMode = true; return; @@ -103,6 +122,73 @@ export class OpenTsQueryCtrl extends QueryCtrl { this.addTag(); } + closeAddTagMode() { + this.addTagMode = false; + return; + } + + addFilter() { + + if (this.target.tags && _.size(this.target.tags) > 0) { + this.errors.filters = "Please remove tags to use filters, tags and filters are mutually exclusive."; + } + + if (!this.addFilterMode) { + this.addFilterMode = true; + return; + } + + if (!this.target.filters) { + this.target.filters = []; + } + + if (!this.target.currentFilterType) { + this.target.currentFilterType = 'iliteral_or'; + } + + if (!this.target.currentFilterGroupBy) { + this.target.currentFilterGroupBy = false; + } + + this.errors = this.validateTarget(); + + if (!this.errors.filters) { + var currentFilter = { + type: this.target.currentFilterType, + tagk: this.target.currentFilterKey, + filter: this.target.currentFilterValue, + groupBy: this.target.currentFilterGroupBy + }; + this.target.filters.push(currentFilter); + this.target.currentFilterType = 'literal_or'; + this.target.currentFilterKey = ''; + this.target.currentFilterValue = ''; + this.target.currentFilterGroupBy = false; + this.targetBlur(); + } + + this.addFilterMode = false; + } + + removeFilter(index) { + this.target.filters.splice(index, 1); + this.targetBlur(); + } + + editFilter(fil, index) { + this.removeFilter(index); + this.target.currentFilterKey = fil.tagk; + this.target.currentFilterValue = fil.filter; + this.target.currentFilterType = fil.type; + this.target.currentFilterGroupBy = fil.groupBy; + this.addFilter(); + } + + closeAddFilterMode() { + this.addFilterMode = false; + return; + } + validateTarget() { var errs: any = {}; diff --git a/public/app/plugins/datasource/opentsdb/specs/datasource-specs.ts b/public/app/plugins/datasource/opentsdb/specs/datasource-specs.ts index b786a93f14c..1da4268e433 100644 --- a/public/app/plugins/datasource/opentsdb/specs/datasource-specs.ts +++ b/public/app/plugins/datasource/opentsdb/specs/datasource-specs.ts @@ -4,7 +4,7 @@ import {OpenTsDatasource} from "../datasource"; describe('opentsdb', function() { var ctx = new helpers.ServiceTestContext(); - var instanceSettings = {url: '' }; + var instanceSettings = {url: '', jsonData: { tsdbVersion: 1 }}; beforeEach(angularMocks.module('grafana.core')); beforeEach(angularMocks.module('grafana.services')); diff --git a/public/app/plugins/datasource/opentsdb/specs/query-ctrl-specs.ts b/public/app/plugins/datasource/opentsdb/specs/query-ctrl-specs.ts new file mode 100644 index 00000000000..5924fac7a38 --- /dev/null +++ b/public/app/plugins/datasource/opentsdb/specs/query-ctrl-specs.ts @@ -0,0 +1,86 @@ +import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common'; +import helpers from 'test/specs/helpers'; +import {OpenTsQueryCtrl} from "../query_ctrl"; + +describe('OpenTsQueryCtrl', function() { + var ctx = new helpers.ControllerTestContext(); + + beforeEach(angularMocks.module('grafana.core')); + beforeEach(angularMocks.module('grafana.services')); + beforeEach(ctx.providePhase(['backendSrv','templateSrv'])); + + beforeEach(ctx.providePhase()); + beforeEach(angularMocks.inject(($rootScope, $controller, $q) => { + ctx.$q = $q; + ctx.scope = $rootScope.$new(); + ctx.target = {target: ''}; + ctx.panelCtrl = {panel: {}}; + ctx.panelCtrl.refresh = sinon.spy(); + ctx.datasource.getAggregators = sinon.stub().returns(ctx.$q.when([])); + ctx.datasource.getFilterTypes = sinon.stub().returns(ctx.$q.when([])); + + ctx.ctrl = $controller(OpenTsQueryCtrl, {$scope: ctx.scope}, { + panelCtrl: ctx.panelCtrl, + datasource: ctx.datasource, + target: ctx.target, + }); + ctx.scope.$digest(); + })); + + describe('init query_ctrl variables', function() { + + it('filter types should be initialized', function() { + expect(ctx.ctrl.filterTypes.length).to.be(7); + }); + + it('aggregators should be initialized', function() { + expect(ctx.ctrl.aggregators.length).to.be(8); + }); + + it('fill policy options should be initialized', function() { + expect(ctx.ctrl.fillPolicies.length).to.be(4); + }); + + }); + + describe('when adding filters and tags', function() { + + it('addTagMode should be false when closed', function() { + ctx.ctrl.addTagMode = true; + ctx.ctrl.closeAddTagMode(); + expect(ctx.ctrl.addTagMode).to.be(false); + }); + + it('addFilterMode should be false when closed', function() { + ctx.ctrl.addFilterMode = true; + ctx.ctrl.closeAddFilterMode(); + expect(ctx.ctrl.addFilterMode).to.be(false); + }); + + it('removing a tag from the tags list', function() { + ctx.ctrl.target.tags = {"tagk": "tag_key", "tagk2": "tag_value2"}; + ctx.ctrl.removeTag("tagk"); + expect(Object.keys(ctx.ctrl.target.tags).length).to.be(1); + }); + + it('removing a filter from the filters list', function() { + ctx.ctrl.target.filters = [{"tagk": "tag_key", "filter": "tag_value2", "type": "wildcard", "groupBy": true}]; + ctx.ctrl.removeFilter(0); + expect(ctx.ctrl.target.filters.length).to.be(0); + }); + + it('adding a filter when tags exist should generate error', function() { + ctx.ctrl.target.tags = {"tagk": "tag_key", "tagk2": "tag_value2"}; + ctx.ctrl.addFilter(); + expect(ctx.ctrl.errors.filters).to.be('Please remove tags to use filters, tags and filters are mutually exclusive.'); + }); + + it('adding a tag when filters exist should generate error', function() { + ctx.ctrl.target.filters = [{"tagk": "tag_key", "filter": "tag_value2", "type": "wildcard", "groupBy": true}]; + ctx.ctrl.addTag(); + expect(ctx.ctrl.errors.tags).to.be('Please remove filters to use tags, tags and filters are mutually exclusive.'); + }); + + }); + +}); diff --git a/public/app/plugins/datasource/prometheus/metric_find_query.js b/public/app/plugins/datasource/prometheus/metric_find_query.js index 65ea92e05b1..25704df1e2c 100644 --- a/public/app/plugins/datasource/prometheus/metric_find_query.js +++ b/public/app/plugins/datasource/prometheus/metric_find_query.js @@ -11,16 +11,16 @@ function (_, moment) { } PrometheusMetricFindQuery.prototype.process = function() { - var label_values_regex = /^label_values\(([^,]+)(?:,\s*(.+))?\)$/; + var label_values_regex = /^label_values\((?:(.+),\s*)?([a-zA-Z_][a-zA-Z0-9_]+)\)$/; var metric_names_regex = /^metrics\((.+)\)$/; var query_result_regex = /^query_result\((.+)\)$/; var label_values_query = this.query.match(label_values_regex); if (label_values_query) { - if (label_values_query[2]) { + if (label_values_query[1]) { return this.labelValuesQuery(label_values_query[2], label_values_query[1]); } else { - return this.labelValuesQuery(label_values_query[1], null); + return this.labelValuesQuery(label_values_query[2], null); } } diff --git a/public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts b/public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts index dfc6b597598..5edf7038cd3 100644 --- a/public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts +++ b/public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts @@ -48,6 +48,22 @@ describe('PrometheusMetricFindQuery', function() { ctx.$rootScope.$apply(); expect(results.length).to.be(3); }); + it('label_values(metric{label1="foo", label2="bar", label3="baz"}, resource) should generate series query', function() { + response = { + status: "success", + data: [ + {__name__: "metric", resource: "value1"}, + {__name__: "metric", resource: "value2"}, + {__name__: "metric", resource: "value3"} + ] + }; + ctx.$httpBackend.expect('GET', 'proxied/api/v1/series?match[]=metric').respond(response); + var pm = new PrometheusMetricFindQuery(ctx.ds, 'label_values(metric, resource)'); + pm.process().then(function(data) { results = data; }); + ctx.$httpBackend.flush(); + ctx.$rootScope.$apply(); + expect(results.length).to.be(3); + }); it('metrics(metric.*) should generate metric name query', function() { response = { status: "success", diff --git a/public/app/plugins/panel/dashlist/module.html b/public/app/plugins/panel/dashlist/module.html index 455291409d5..79952d0032c 100644 --- a/public/app/plugins/panel/dashlist/module.html +++ b/public/app/plugins/panel/dashlist/module.html @@ -5,7 +5,7 @@ {{dash.title}} - +
diff --git a/public/app/plugins/panel/dashlist/module.ts b/public/app/plugins/panel/dashlist/module.ts index a1c72434b38..fd0e905d923 100644 --- a/public/app/plugins/panel/dashlist/module.ts +++ b/public/app/plugins/panel/dashlist/module.ts @@ -3,6 +3,7 @@ import _ from 'lodash'; import config from 'app/core/config'; import {PanelCtrl} from 'app/plugins/sdk'; +import {impressions} from 'app/features/dashboard/impressionStore'; // Set and populate defaults var panelDefaults = { @@ -31,7 +32,7 @@ class DashListCtrl extends PanelCtrl { initEditMode() { super.initEditMode(); - this.modes = ['starred', 'search']; + this.modes = ['starred', 'search', 'last viewed']; this.icon = "fa fa-star"; this.addEditorTab('Options', () => { return {templateUrl: 'public/app/plugins/panel/dashlist/editor.html'}; @@ -41,6 +42,19 @@ class DashListCtrl extends PanelCtrl { refresh() { var params: any = {limit: this.panel.limit}; + if (this.panel.mode === 'last viewed') { + var dashListNames = _.first(impressions.getDashboardOpened(), this.panel.limit).map((dashboard) => { + return { + title: dashboard, + uri: 'db/' + dashboard + }; + }); + + this.dashList = dashListNames; + this.renderingCompleted(); + return; + } + if (this.panel.mode === 'starred') { params.starred = "true"; } else { diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index 1c022d460bb..357da9b6877 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -125,7 +125,8 @@ class GraphCtrl extends MetricsPanelCtrl { getExtendedMenu() { var menu = super.getExtendedMenu(); - menu.push({text: 'Export CSV', click: 'ctrl.exportCsv()'}); + menu.push({text: 'Export CSV (series as rows)', click: 'ctrl.exportCsv()'}); + menu.push({text: 'Export CSV (series as columns)', click: 'ctrl.exportCsvColumns()'}); menu.push({text: 'Toggle legend', click: 'ctrl.toggleLegend()'}); return menu; } @@ -295,6 +296,10 @@ class GraphCtrl extends MetricsPanelCtrl { exportCsv() { fileExport.exportSeriesListToCsv(this.seriesList); } + + exportCsvColumns() { + fileExport.exportSeriesListToCsvColumns(this.seriesList); + } } export {GraphCtrl, GraphCtrl as PanelCtrl} diff --git a/public/app/plugins/panel/singlestat/editor.html b/public/app/plugins/panel/singlestat/editor.html index bf3227f7582..6b0806133b8 100644 --- a/public/app/plugins/panel/singlestat/editor.html +++ b/public/app/plugins/panel/singlestat/editor.html @@ -167,13 +167,13 @@
  • - +
  • - +
  • diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index 7f576cc3f2d..bb7b9cec0b6 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -50,7 +50,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { unitFormats: any[]; /** @ngInject */ - constructor($scope, $injector, private $location, private linkSrv, private templateSrv) { + constructor($scope, $injector, private $location, private linkSrv) { super($scope, $injector); _.defaults(this.panel, panelDefaults); } @@ -213,7 +213,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { // value/number to text mapping var value = parseFloat(map.value); - if (value === data.value) { + if (value === data.valueRounded) { data.valueFormated = map.text; return; } diff --git a/public/app/plugins/panel/singlestat/specs/singlestat-specs.ts b/public/app/plugins/panel/singlestat/specs/singlestat-specs.ts index 283389ee400..90bd5339737 100644 --- a/public/app/plugins/panel/singlestat/specs/singlestat-specs.ts +++ b/public/app/plugins/panel/singlestat/specs/singlestat-specs.ts @@ -69,14 +69,20 @@ describe('SingleStatCtrl', function() { singleStatScenario('When value to text mapping is specified', function(ctx) { ctx.setup(function() { - ctx.datapoints = [[10,1]]; + ctx.datapoints = [[9.9,1]]; ctx.ctrl.panel.valueMaps = [{value: '10', text: 'OK'}]; }); - it('Should replace value with text', function() { - expect(ctx.data.value).to.be(10); - expect(ctx.data.valueFormated).to.be('OK'); + it('value should remain', function() { + expect(ctx.data.value).to.be(9.9); }); + it('round should be rounded up', function() { + expect(ctx.data.valueRounded).to.be(10); + }); + + it('Should replace value with text', function() { + expect(ctx.data.valueFormated).to.be('OK'); + }); }); }); diff --git a/public/dashboards/home.json b/public/dashboards/home.json index e05a882ef57..d54e276db5c 100644 --- a/public/dashboards/home.json +++ b/public/dashboards/home.json @@ -47,11 +47,11 @@ { "id": 3, "limit": 10, - "mode": "search", + "mode": "last viewed", "query": "", "span": 6, "tags": [], - "title": "Dashboards", + "title": "Last 10 viewed dashboards", "type": "dashlist" } ], diff --git a/public/sass/base/_fonts.scss b/public/sass/base/_fonts.scss index f43721afb3a..eece414d45d 100644 --- a/public/sass/base/_fonts.scss +++ b/public/sass/base/_fonts.scss @@ -8,7 +8,7 @@ font-weight: normal; font-style: normal; } - + .icon-gf { /* use !important to prevent issues with browser extensions that change fonts */ font-family: 'grafana-icons' !important; @@ -28,10 +28,6 @@ vertical-align: middle; } - - - - .icon-gf-raintank_wordmark:before { content: "\e600"; } diff --git a/public/sass/base/_type.scss b/public/sass/base/_type.scss index b26f5493d8e..7cdf3de06aa 100644 --- a/public/sass/base/_type.scss +++ b/public/sass/base/_type.scss @@ -131,7 +131,7 @@ mark, // Unordered and Ordered lists ul, ol { padding: 0; - margin: 0 0 $line-height-base / 2 25px; + padding-left: $spacer; } ul ul, ul ol, diff --git a/public/sass/components/_color_picker.scss b/public/sass/components/_color_picker.scss index 9e8adfd6226..d5f04265fc8 100644 --- a/public/sass/components/_color_picker.scss +++ b/public/sass/components/_color_picker.scss @@ -31,7 +31,7 @@ width: 15px; height: 15px; border: none; - margin-right: 5px; + margin: 0; float: left; z-index: 0; } diff --git a/public/test/core/utils/kbn_specs.js b/public/test/core/utils/kbn_specs.js index 23c752fe471..7e75e880546 100644 --- a/public/test/core/utils/kbn_specs.js +++ b/public/test/core/utils/kbn_specs.js @@ -127,7 +127,7 @@ define([ it('10m 1600 resolution', function() { var range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') }; var str = kbn.calculateInterval(range, 1600, null); - expect(str).to.be('100ms'); + expect(str).to.be('500ms'); }); it('fixed user interval', function() { @@ -145,7 +145,7 @@ define([ it('large time range and user low limit', function() { var range = { from: dateMath.parse('now-14d'), to: dateMath.parse('now') }; var str = kbn.calculateInterval(range, 1000, '>10s'); - expect(str).to.be('30m'); + expect(str).to.be('20m'); }); }); });