mirror of
https://github.com/grafana/grafana.git
synced 2024-11-25 18:30:41 -06:00
Merge branch 'master' into variable-value-formatting-rethink
This commit is contained in:
commit
f44d6e063c
@ -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)
|
||||
|
2
docker/blocks/elastic/elasticsearch.yml
Normal file
2
docker/blocks/elastic/elasticsearch.yml
Normal file
@ -0,0 +1,2 @@
|
||||
script.inline: on
|
||||
script.indexed: on
|
@ -1 +0,0 @@
|
||||
Ensure the existence of the parent folder.
|
@ -4,3 +4,5 @@ elasticsearch:
|
||||
ports:
|
||||
- "9200:9200"
|
||||
- "9300:9300"
|
||||
volumes:
|
||||
- ./blocks/elastic/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
};
|
||||
|
||||
|
@ -76,8 +76,8 @@
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label">
|
||||
<span>Color</span>
|
||||
<spectrum-picker ng-model="currentAnnotation.iconColor"></spectrum-picker>
|
||||
</label>
|
||||
<spectrum-picker class="gf-form-input" ng-model="currentAnnotation.iconColor"></spectrum-picker>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -16,4 +16,5 @@ define([
|
||||
'./graphiteImportCtrl',
|
||||
'./dynamicDashboardSrv',
|
||||
'./importCtrl',
|
||||
'./impressionStore',
|
||||
], function () {});
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
41
public/app/features/dashboard/impressionStore.ts
Normal file
41
public/app/features/dashboard/impressionStore.ts
Normal file
@ -0,0 +1,41 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
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
|
||||
};
|
@ -1,6 +1,6 @@
|
||||
<div class="submenu-controls">
|
||||
<ul ng-if="ctrl.dashboard.templating.list.length > 0">
|
||||
<li ng-repeat="variable in ctrl.variables" class="submenu-item">
|
||||
<li ng-repeat="variable in ctrl.variables" ng-show="!variable.hideVariable" class="submenu-item">
|
||||
<span class="submenu-item-label template-variable " ng-show="!variable.hideLabel">
|
||||
{{variable.label || variable.name}}:
|
||||
</span>
|
||||
|
@ -23,6 +23,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th><strong>name</strong></th>
|
||||
<th><strong>type</strong></th>
|
||||
<th><strong>url</strong></th>
|
||||
<th style="width: 60px;"></th>
|
||||
<th style="width: 85px;"></th>
|
||||
@ -37,7 +38,10 @@
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<span class="ellipsis">{{ds.url}}</span>
|
||||
<span>{{ds.type}}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{ds.url}}</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span ng-if="ds.isDefault">
|
||||
|
@ -17,6 +17,7 @@ class MetricsPanelCtrl extends PanelCtrl {
|
||||
$timeout: any;
|
||||
datasourceSrv: any;
|
||||
timeSrv: any;
|
||||
templateSrv: any;
|
||||
timing: any;
|
||||
range: any;
|
||||
rangeRaw: any;
|
||||
@ -34,6 +35,7 @@ class MetricsPanelCtrl extends PanelCtrl {
|
||||
this.$q = $injector.get('$q');
|
||||
this.datasourceSrv = $injector.get('datasourceSrv');
|
||||
this.timeSrv = $injector.get('timeSrv');
|
||||
this.templateSrv = $injector.get('templateSrv');
|
||||
|
||||
if (!this.panel.targets) {
|
||||
this.panel.targets = [{}];
|
||||
@ -119,7 +121,8 @@ class MetricsPanelCtrl extends PanelCtrl {
|
||||
|
||||
// check panel time overrrides
|
||||
if (this.panel.timeFrom) {
|
||||
var timeFromInfo = rangeUtil.describeTextRange(this.panel.timeFrom);
|
||||
var timeFromInterpolated = this.templateSrv.replace(this.panel.timeFrom, this.panel.scopedVars);
|
||||
var timeFromInfo = rangeUtil.describeTextRange(timeFromInterpolated);
|
||||
if (timeFromInfo.invalid) {
|
||||
this.timeInfo = 'invalid time override';
|
||||
return;
|
||||
@ -136,13 +139,14 @@ class MetricsPanelCtrl extends PanelCtrl {
|
||||
}
|
||||
|
||||
if (this.panel.timeShift) {
|
||||
var timeShiftInfo = rangeUtil.describeTextRange(this.panel.timeShift);
|
||||
var timeShiftInterpolated = this.templateSrv.replace(this.panel.timeShift, this.panel.scopedVars);
|
||||
var timeShiftInfo = rangeUtil.describeTextRange(timeShiftInterpolated);
|
||||
if (timeShiftInfo.invalid) {
|
||||
this.timeInfo = 'invalid timeshift';
|
||||
return;
|
||||
}
|
||||
|
||||
var timeShift = '-' + this.panel.timeShift;
|
||||
var timeShift = '-' + timeShiftInterpolated;
|
||||
this.timeInfo += ' timeshift ' + timeShift;
|
||||
this.range.from = dateMath.parseDateMath(timeShift, this.range.from, false);
|
||||
this.range.to = dateMath.parseDateMath(timeShift, this.range.to, true);
|
||||
|
@ -31,7 +31,6 @@ var panelTemplate = `
|
||||
<div class="tabbed-view tabbed-view--panel-edit">
|
||||
<div class="tabbed-view-header">
|
||||
<h2 class="tabbed-view-title">
|
||||
<i ng-class="ctrl.icon"></i>
|
||||
{{ctrl.pluginName}}
|
||||
</h2>
|
||||
|
||||
|
@ -5,14 +5,13 @@ import _ from 'lodash';
|
||||
|
||||
export class AppPageCtrl {
|
||||
page: any;
|
||||
appId: any;
|
||||
pluginId: any;
|
||||
appModel: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private backendSrv, private $routeParams: any, private $rootScope) {
|
||||
this.appId = $routeParams.appId;
|
||||
|
||||
this.backendSrv.get(`/api/org/apps/${this.appId}/settings`).then(app => {
|
||||
this.pluginId = $routeParams.pluginId;
|
||||
this.backendSrv.get(`/api/org/plugins/${this.pluginId}/settings`).then(app => {
|
||||
this.appModel = app;
|
||||
this.page = _.findWhere(app.pages, {slug: this.$routeParams.slug});
|
||||
if (!this.page) {
|
||||
|
@ -72,7 +72,7 @@
|
||||
{{ds.name}}
|
||||
</li>
|
||||
<li ng-repeat="page in ctrl.model.pages">
|
||||
<a href="apps/{{ctrl.appId}}/page/{{page.slug}}" class="external-link">{{page.name}}</a>
|
||||
<a href="plugins/{{ctrl.pluginId}}/page/{{page.slug}}" class="external-link">{{page.name}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
@ -1,4 +1,4 @@
|
||||
<navbar icon="icon-gf icon-gf-apps" title="{{ctrl.appModel.name}}" title-url="apps/{{ctrl.appId}}/edit">
|
||||
<navbar icon="icon-gf icon-gf-apps" title="{{ctrl.appModel.name}}" title-url="plugins/{{ctrl.pluginId}}/edit">
|
||||
</navbar>
|
||||
|
||||
<div class="page-container">
|
||||
|
@ -91,11 +91,17 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-7">Label</span>
|
||||
<input type="text" class="gf-form-input max-width-14" ng-model='current.label' placeholder="optional display name"></input>
|
||||
<editor-checkbox class="width-13" text="Hide label" model="current.hideLabel" change="runQuery()"></editor-checkbox>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-7">Label</span>
|
||||
<input type="text" class="gf-form-input max-width-14" ng-model='current.label' placeholder="optional display name"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<editor-checkbox class="width-10" text="Hide label" model="current.hideLabel" change="runQuery()"></editor-checkbox>
|
||||
<editor-checkbox class="width-11" text="Hide variable" model="current.hideVariable" change="runQuery()"></editor-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<h5 class="section-heading">Value Options</h5>
|
||||
@ -109,10 +115,16 @@
|
||||
<span class="gf-form-label" ng-show="current.auto">
|
||||
Auto interval steps <tip>How many times should the current time range be divided to calculate the value</tip>
|
||||
</span>
|
||||
<div class="gf-form-select-wrapper max-width-10">
|
||||
<select class="gf-form-input" ng-model="current.auto_count" ng-options="f for f in [3,5,10,30,50,100,200]" ng-change="runQuery()"></select>
|
||||
<div class="gf-form-select-wrapper max-width-10" ng-show="current.auto">
|
||||
<select class="gf-form-input" ng-model="current.auto_count" ng-options="f for f in [2,3,4,5,10,20,30,40,50,100,200,300,400,500]" ng-change="runQuery()"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label" ng-show="current.auto">
|
||||
Auto interval min value <tip>The calculated value will not go below this threshold</tip>
|
||||
</span>
|
||||
<input type="text" class="gf-form-input max-width-10" ng-show="current.auto" ng-model="current.auto_min" ng-change="runQuery()"></input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="current.type === 'custom'" class="gf-form-group">
|
||||
|
@ -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);
|
||||
};
|
||||
|
||||
|
@ -18,8 +18,8 @@
|
||||
<div class="gf-box-body">
|
||||
|
||||
<div ng-if="editor.index == 0">
|
||||
<h5>Request details</h5>
|
||||
<table class="table table-striped small inspector-request-table">
|
||||
<h5 class="section-heading">Request details</h5>
|
||||
<table class="filter-table gf-form-group">
|
||||
<tr>
|
||||
<td>Url</td>
|
||||
<td>{{inspector.error.config.url}}</td>
|
||||
@ -38,8 +38,8 @@
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h5>Request parameters</h5>
|
||||
<table class="table table-striped small inspector-request-table">
|
||||
<h5 class="section-heading">Request parameters</h5>
|
||||
<table class="filter-table">
|
||||
<tr ng-repeat="param in request_parameters">
|
||||
<td>
|
||||
{{param.key}}
|
||||
|
@ -37,7 +37,7 @@
|
||||
|
||||
</div>
|
||||
|
||||
<div class="editor-row" style="margin-top: 30px">
|
||||
<div class="editor-row">
|
||||
|
||||
<div class="pull-right dropdown" style="margin-right: 10px;">
|
||||
<button class="btn btn-inverse dropdown-toggle" data-toggle="dropdown" bs-tooltip="'Datasource'">
|
||||
|
21
public/app/plugins/datasource/opentsdb/config_ctrl.ts
Normal file
21
public/app/plugins/datasource/opentsdb/config_ctrl.ts
Normal file
@ -0,0 +1,21 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
|
||||
export class OpenTsConfigCtrl {
|
||||
static templateUrl = 'public/app/plugins/datasource/opentsdb/partials/config.html';
|
||||
current: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor($scope) {
|
||||
this.current.jsonData = this.current.jsonData || {};
|
||||
this.current.jsonData.tsdbVersion = this.current.jsonData.tsdbVersion || 1;
|
||||
}
|
||||
|
||||
tsdbVersions = [
|
||||
{name: '<=2.1', value: 1},
|
||||
{name: '2.2', value: 2},
|
||||
];
|
||||
|
||||
}
|
@ -14,6 +14,8 @@ function (angular, _, dateMath) {
|
||||
this.name = instanceSettings.name;
|
||||
this.withCredentials = instanceSettings.withCredentials;
|
||||
this.basicAuth = instanceSettings.basicAuth;
|
||||
instanceSettings.jsonData = instanceSettings.jsonData || {};
|
||||
this.tsdbVersion = instanceSettings.jsonData.tsdbVersion || 1;
|
||||
this.supportMetrics = true;
|
||||
this.tagKeys = {};
|
||||
|
||||
@ -39,9 +41,15 @@ function (angular, _, dateMath) {
|
||||
|
||||
var groupByTags = {};
|
||||
_.each(queries, function(query) {
|
||||
_.each(query.tags, function(val, key) {
|
||||
groupByTags[key] = true;
|
||||
});
|
||||
if (query.filters && query.filters.length > 0) {
|
||||
_.each(query.filters, function(val) {
|
||||
groupByTags[val.tagk] = true;
|
||||
});
|
||||
} else {
|
||||
_.each(query.tags, function(val, key) {
|
||||
groupByTags[key] = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return this.performTimeSeriesQuery(queries, start, end).then(function(response) {
|
||||
@ -88,6 +96,7 @@ function (angular, _, dateMath) {
|
||||
// In case the backend is 3rd-party hosted and does not suport OPTIONS, urlencoded requests
|
||||
// go as POST rather than OPTIONS+POST
|
||||
options.headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
|
||||
|
||||
return backendSrv.datasourceRequest(options);
|
||||
};
|
||||
|
||||
@ -215,7 +224,7 @@ function (angular, _, dateMath) {
|
||||
this.getAggregators = function() {
|
||||
if (aggregatorsPromise) { return aggregatorsPromise; }
|
||||
|
||||
aggregatorsPromise = this._get('/api/aggregators').then(function(result) {
|
||||
aggregatorsPromise = this._get('/api/aggregators').then(function(result) {
|
||||
if (result.data && _.isArray(result.data)) {
|
||||
return result.data.sort();
|
||||
}
|
||||
@ -224,6 +233,19 @@ function (angular, _, dateMath) {
|
||||
return aggregatorsPromise;
|
||||
};
|
||||
|
||||
var filterTypesPromise = null;
|
||||
this.getFilterTypes = function() {
|
||||
if (filterTypesPromise) { return filterTypesPromise; }
|
||||
|
||||
filterTypesPromise = this._get('/api/config/filters').then(function(result) {
|
||||
if (result.data) {
|
||||
return Object.keys(result.data).sort();
|
||||
}
|
||||
return [];
|
||||
});
|
||||
return filterTypesPromise;
|
||||
};
|
||||
|
||||
function transformMetricData(md, groupByTags, target, options) {
|
||||
var metricLabel = createMetricLabel(md, target, groupByTags, options);
|
||||
var dps = [];
|
||||
@ -307,10 +329,14 @@ function (angular, _, dateMath) {
|
||||
}
|
||||
}
|
||||
|
||||
query.tags = angular.copy(target.tags);
|
||||
if(query.tags){
|
||||
for(var key in query.tags){
|
||||
query.tags[key] = templateSrv.replace(query.tags[key], options.scopedVars);
|
||||
if (target.filters && target.filters.length > 0) {
|
||||
query.filters = angular.copy(target.filters);
|
||||
} else {
|
||||
query.tags = angular.copy(target.tags);
|
||||
if(query.tags){
|
||||
for(var key in query.tags){
|
||||
query.tags[key] = templateSrv.replace(query.tags[key], options.scopedVars);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -321,11 +347,18 @@ function (angular, _, dateMath) {
|
||||
var interpolatedTagValue;
|
||||
return _.map(metrics, function(metricData) {
|
||||
return _.findIndex(options.targets, function(target) {
|
||||
return target.metric === metricData.metric &&
|
||||
if (target.filters && target.filters.length > 0) {
|
||||
return target.metric === metricData.metric &&
|
||||
_.all(target.filters, function(filter) {
|
||||
return filter.tagk === interpolatedTagValue === "*";
|
||||
});
|
||||
} else {
|
||||
return target.metric === metricData.metric &&
|
||||
_.all(target.tags, function(tagV, tagK) {
|
||||
interpolatedTagValue = templateSrv.replace(tagV, options.scopedVars);
|
||||
return metricData.tags[tagK] === interpolatedTagValue || interpolatedTagValue === "*";
|
||||
});
|
||||
interpolatedTagValue = templateSrv.replace(tagV, options.scopedVars);
|
||||
return metricData.tags[tagK] === interpolatedTagValue || interpolatedTagValue === "*";
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -1,9 +1,6 @@
|
||||
import {OpenTsDatasource} from './datasource';
|
||||
import {OpenTsQueryCtrl} from './query_ctrl';
|
||||
|
||||
class OpenTsConfigCtrl {
|
||||
static templateUrl = 'partials/config.html';
|
||||
}
|
||||
import {OpenTsConfigCtrl} from './config_ctrl';
|
||||
|
||||
export {
|
||||
OpenTsDatasource as Datasource,
|
||||
|
@ -1,2 +1,13 @@
|
||||
<datasource-http-settings current="ctrl.current"></datasource-http-settings>
|
||||
|
||||
<br>
|
||||
<h5>Opentsdb settings</h5>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-7">
|
||||
Version
|
||||
</span>
|
||||
<span class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input gf-size-auto" ng-model="ctrl.current.jsonData.tsdbVersion" ng-options="v.value as v.name for v in ctrl.tsdbVersions"></select>
|
||||
</span>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
@ -63,12 +63,11 @@
|
||||
</select>
|
||||
</li>
|
||||
|
||||
<li class="tight-form-item query-keyword" style="width: 59px">
|
||||
<li class="tight-form-item query-keyword" style="width: 59px" ng-if="ctrl.tsdbVersion == 2">
|
||||
Fill
|
||||
<tip>Available since OpenTSDB 2.2</tip>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<li ng-if="ctrl.tsdbVersion == 2">
|
||||
<select ng-model="ctrl.target.downsampleFillPolicy" class="tight-form-input input-small"
|
||||
ng-options="agg for agg in ctrl.fillPolicies"
|
||||
ng-change="ctrl.targetBlur()">
|
||||
@ -83,10 +82,67 @@
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
<div class="tight-form" ng-if="ctrl.tsdbVersion == 2">
|
||||
<ul class="tight-form-list" role="menu">
|
||||
<li class="tight-form-item tight-form-align query-keyword" style="width: 100px">
|
||||
Filters
|
||||
<tip ng-if="ctrl.tsdbVersion == 2">Filters does not work with tags, either of the two will work but not both.</tip>
|
||||
</li>
|
||||
<li ng-repeat="fil in ctrl.target.filters track by $index" class="tight-form-item">
|
||||
{{fil.tagk}} = {{fil.type}}({{fil.filter}}) , groupBy = {{fil.groupBy}}
|
||||
<a ng-click="ctrl.editFilter(fil, $index)">
|
||||
<i class="fa fa-pencil"></i>
|
||||
</a>
|
||||
<a ng-click="ctrl.removeFilter($index)">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li class="tight-form-item query-keyword" ng-hide="ctrl.addFilterMode">
|
||||
<a ng-click="ctrl.addFilter()">
|
||||
<i class="fa fa-plus"></i>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="query-keyword" ng-show="ctrl.addFilterMode">
|
||||
<input type="text" class="input-small tight-form-input" spellcheck='false'
|
||||
bs-typeahead="ctrl.suggestTagKeys" data-min-length=0 data-items=100
|
||||
ng-model="ctrl.target.currentFilterKey" placeholder="key"></input>
|
||||
|
||||
Type <select ng-model="ctrl.target.currentFilterType"
|
||||
class="tight-form-input input-small"
|
||||
ng-options="filType for filType in ctrl.filterTypes">
|
||||
</select>
|
||||
|
||||
<input type="text" class="input-small tight-form-input"
|
||||
spellcheck='false' bs-typeahead="ctrl.suggestTagValues"
|
||||
data-min-length=0 data-items=100 ng-model="ctrl.target.currentFilterValue" placeholder="filter">
|
||||
</input>
|
||||
|
||||
groupBy <editor-checkbox text="" model="ctrl.target.currentFilterGroupBy"></editor-checkbox>
|
||||
|
||||
<a bs-tooltip="ctrl.errors.filters"
|
||||
style="color: rgb(229, 189, 28)"
|
||||
ng-show="ctrl.errors.filters">
|
||||
<i class="fa fa-warning"></i>
|
||||
</a>
|
||||
|
||||
<a ng-click="ctrl.addFilter()" ng-hide="ctrl.errors.filters">
|
||||
add filter
|
||||
</a>
|
||||
<a ng-click="ctrl.closeAddFilterMode()">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list" role="menu">
|
||||
<li class="tight-form-item tight-form-align query-keyword" style="width: 100px">
|
||||
Tags
|
||||
<tip ng-if="ctrl.tsdbVersion == 2">Please use filters, tags are deprecated in opentsdb 2.2</tip>
|
||||
</li>
|
||||
<li ng-repeat="(key, value) in ctrl.target.tags track by $index" class="tight-form-item">
|
||||
{{key}} = {{value}}
|
||||
@ -113,15 +169,21 @@
|
||||
spellcheck='false' bs-typeahead="ctrl.suggestTagValues"
|
||||
data-min-length=0 data-items=100 ng-model="ctrl.target.currentTagValue" placeholder="value">
|
||||
</input>
|
||||
<a ng-click="ctrl.addTag()">
|
||||
|
||||
<a bs-tooltip="ctrl.errors.tags"
|
||||
style="color: rgb(229, 189, 28)"
|
||||
ng-show="ctrl.errors.tags">
|
||||
<i class="fa fa-warning"></i>
|
||||
</a>
|
||||
|
||||
<a ng-click="ctrl.addTag()" ng-hide="ctrl.errors.tags">
|
||||
add tag
|
||||
</a>
|
||||
<a bs-tooltip="ctrl.errors.tags"
|
||||
style="color: rgb(229, 189, 28)"
|
||||
ng-show="target.errors.tags">
|
||||
<i class="fa fa-warning"></i>
|
||||
</a>
|
||||
</li>
|
||||
<a ng-click="ctrl.closeAddTagMode()">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
@ -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 = {};
|
||||
|
||||
|
@ -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'));
|
||||
|
@ -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.');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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",
|
||||
|
@ -5,7 +5,7 @@
|
||||
{{dash.title}}
|
||||
</span>
|
||||
<span class="dashlist-star">
|
||||
<i class="fa" ng-class="{'fa-star': dash.isStarred, 'fa-star-o': !dash.isStarred}"></i>
|
||||
<i class="fa" ng-class="{'fa-star': dash.isStarred, 'fa-star-o': dash.isStarred === false}"></i>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -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 {
|
||||
|
@ -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}
|
||||
|
@ -167,13 +167,13 @@
|
||||
<i class="fa fa-remove pointer" ng-click="ctrl.removeValueMap(map)"></i>
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" ng-model="ctrl.map.value" placeholder="value" class="input-mini tight-form-input" ng-blur="ctrl.render()">
|
||||
<input type="text" ng-model="map.value" placeholder="value" class="input-mini tight-form-input" ng-blur="ctrl.render()">
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
<i class="fa fa-arrow-right"></i>
|
||||
</li>
|
||||
<li ng-repeat-end>
|
||||
<input type="text" placeholder="text" ng-model="ctrl.map.text" class="input-mini tight-form-input" ng-blur="ctrl.render()">
|
||||
<input type="text" placeholder="text" ng-model="map.text" class="input-mini tight-form-input" ng-blur="ctrl.render()">
|
||||
</li>
|
||||
|
||||
<li>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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"
|
||||
}
|
||||
],
|
||||
|
@ -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";
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -31,7 +31,7 @@
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border: none;
|
||||
margin-right: 5px;
|
||||
margin: 0;
|
||||
float: left;
|
||||
z-index: 0;
|
||||
}
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user