Compare commits

...

26 Commits

Author SHA1 Message Date
bergquist
a8aa16673e release 5.3.3 2018-11-06 16:49:03 +01:00
Marcus Efraimsson
0d821d07ad Merge pull request #13807 from bergquist/cp_v5.3.x
Cherry-pick commit for 5.3.2
2018-10-24 14:06:27 +02:00
bergquist
f7092fa6b0 delete provisioning meta data when deleting folder
prior to this fix Grafana didnt delete meta data
about the provisioned dashboard in `dashboard_provisioning`
which means that the dashboard wasn't inserted into
Grafana again if the folder was delete within Grafana.

closes #13280

(cherry picked from commit 0a9bfc5529)
2018-10-24 13:57:19 +02:00
Marcus Efraimsson
9ca86f1b0e Merge pull request #13790 from grafana/cp-5.3.2
Cherry picks for v5.3.2
2018-10-24 13:43:58 +02:00
Marcus Efraimsson
dc70298210 release 5.3.2 2018-10-24 13:27:02 +02:00
Erik Sundell
d20c7260b3 Resource type filter (#13784)
* stackdriver: add resource type to filter and group bys

* stackdriver: remove not used param

* stackdriver: refactor filter and group by code

* stackdriver: remove resource type if its already in filter list

* stackdriver: remove debug logging

* stackdriver: remove more debug logging

* stackdriver: append resource type to legend name if there are more than one type present in the response

* stackdriver: only make new request if filter has real value

* stackdriver: format legend support for resource type

* stackdriver: add resource type to documentation

* stackdriver: not returning promise from query function

* stackdriver: fix refactoring bug

* stackdriver: remove not used import

(cherry picked from commit c5af0bf1c5)
2018-10-24 13:26:14 +02:00
Johannes Schill
a54fa3858e Move the variable regex to constants to make sure we use the same reg… (#13801)
(cherry picked from commit 38c155403e)
2018-10-24 12:09:46 +02:00
Torkel Ödegaard
7be0716752 fix: another fix for #13764 , #13793
(cherry picked from commit 53d9619cb9)
2018-10-24 09:05:04 +02:00
Torkel Ödegaard
cb1eedb5f7 fix: kiosk url fix, fixes #13764
(cherry picked from commit 8a1e0cd83b)
2018-10-24 09:04:44 +02:00
Leonard Gram
0d8c7573f3 docker: adds curl back into the docker image for utility. (#13794)
(cherry picked from commit 4cc89f1753)
2018-10-23 16:16:57 +02:00
Adam Palaniuk
c9591f8a8c Update check for invalid percentile statistics
(cherry picked from commit 58a156ba03)
2018-10-23 14:20:10 +02:00
Johannes Schill
6c3202b1b6 fix: Text box variables with empty values should not be considered fa… (#13791)
* fix: text box template variable doesn't work properly without a default value

(cherry picked from commit 22a0f3cf94)
2018-10-23 14:07:04 +02:00
Marcus Efraimsson
5daf842431 add debug logging of folder/dashbord permission checks
(cherry picked from commit b371f2d91f)
2018-10-23 11:51:05 +02:00
Erik Sundell
487a8585c6 stackdriver: only add unit to resonse obj if it has a value
(cherry picked from commit b2932058c7)
2018-10-23 11:20:35 +02:00
Erik Sundell
20a47ed3d6 stackdriver: fix failing tests
(cherry picked from commit 0f0763b6b8)
2018-10-23 11:20:27 +02:00
Erik Sundell
72e60346bc stackdriver: make sure unit is not returned to the panel if mapping from stackdriver unit to grafana unit can't be made
(cherry picked from commit d1740f090a)
2018-10-23 11:20:19 +02:00
Mitsuhiro Tanda
f213f664ce allow unit override if cloudwatch response unit is none
(cherry picked from commit 4687ce2f7b)
2018-10-23 11:20:10 +02:00
Mitsuhiro Tanda
5b9116bf80 Revert "don't overwrite unit if user set"
This reverts commit 9dd33b79e037fc75ddc5f3a6b294edba99e99b94.

(cherry picked from commit e465b2d53a)
2018-10-23 11:19:32 +02:00
Mitsuhiro Tanda
3891b82443 don't overwrite unit if user set
(cherry picked from commit 287ba77abf)
2018-10-23 11:19:24 +02:00
Sven Klemm
7ddccdba08 Fix variable highlighting
(cherry picked from commit 2803cdca40)
2018-10-23 11:18:50 +02:00
Sven Klemm
02b4cf392d Escape typeahead values in query_part
(cherry picked from commit 20c1a58488)
2018-10-23 11:18:43 +02:00
Sven Klemm
112fa2b8b9 Escape values in metric segment and sql part
(cherry picked from commit 3a25a0de83)
2018-10-23 11:18:34 +02:00
Adrien Fillon
e8e8b014f6 fix LDAP Grafana admin logic
Co-authored-by: Adrien Fillon <adrien.fillon@cdiscount.com>
Co-authored-by: Remi Buisson <remi.buisson@cdiscount.com>
(cherry picked from commit 781e66ba3c)
2018-10-23 11:18:04 +02:00
Mitsuhiro Tanda
02a3e11708 fix concurrent map writes
(cherry picked from commit 48aef0c50e)
2018-10-23 11:17:39 +02:00
Marcus Efraimsson
b0f91f3a3e postgres: use arrow function declaration of interpolateVariable
(cherry picked from commit 7b656097a7)
2018-10-23 11:17:07 +02:00
Sven Klemm
99a8bf2195 Use closure for calling interpolateVariable
(cherry picked from commit ec0fd96f08)
2018-10-23 11:16:58 +02:00
27 changed files with 272 additions and 118 deletions

View File

@@ -134,6 +134,16 @@ Example Alias By: `{{metric.type}} - {{metric.labels.instance_name}}`
Example Result: `compute.googleapis.com/instance/cpu/usage_time - server1-prod`
It is also possible to resolve the name of the Monitored Resource Type.
| Alias Pattern Format | Description | Example Result |
| ------------------------ | ------------------------------------------------| ---------------- |
| `{{resource.type}}` | returns the name of the monitored resource type | `gce_instance` |
Example Alias By: `{{resource.type}} - {{metric.type}}`
Example Result: `gce_instance - compute.googleapis.com/instance/cpu/usage_time`
## Templating
Instead of hard-coding things like server, application and sensor name in you metric queries you can use variables in their place.

View File

@@ -4,7 +4,7 @@
"company": "Grafana Labs"
},
"name": "grafana",
"version": "5.3.1",
"version": "5.3.3",
"repository": {
"type": "git",
"url": "http://github.com/grafana/grafana.git"

View File

@@ -25,7 +25,7 @@ ENV PATH=/usr/share/grafana/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bi
WORKDIR $GF_PATHS_HOME
RUN apt-get update && apt-get install -qq -y libfontconfig ca-certificates && \
RUN apt-get update && apt-get install -qq -y libfontconfig ca-certificates curl && \
apt-get autoremove -y && \
rm -rf /var/lib/apt/lists/*

View File

@@ -185,7 +185,9 @@ func (a *ldapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo
if ldapUser.isMemberOf(group.GroupDN) {
extUser.OrgRoles[group.OrgId] = group.OrgRole
extUser.IsGrafanaAdmin = group.IsGrafanaAdmin
if extUser.IsGrafanaAdmin == nil || *extUser.IsGrafanaAdmin == false {
extUser.IsGrafanaAdmin = group.IsGrafanaAdmin
}
}
}

View File

@@ -40,7 +40,7 @@ var New = func(dashId int64, orgId int64, user *m.SignedInUser) DashboardGuardia
user: user,
dashId: dashId,
orgId: orgId,
log: log.New("guardians.dashboard"),
log: log.New("dashboard.permissions"),
}
}
@@ -66,15 +66,30 @@ func (g *dashboardGuardianImpl) CanAdmin() (bool, error) {
func (g *dashboardGuardianImpl) HasPermission(permission m.PermissionType) (bool, error) {
if g.user.OrgRole == m.ROLE_ADMIN {
return true, nil
return g.logHasPermissionResult(permission, true, nil)
}
acl, err := g.GetAcl()
if err != nil {
return false, err
return g.logHasPermissionResult(permission, false, err)
}
return g.checkAcl(permission, acl)
result, err := g.checkAcl(permission, acl)
return g.logHasPermissionResult(permission, result, err)
}
func (g *dashboardGuardianImpl) logHasPermissionResult(permission m.PermissionType, hasPermission bool, err error) (bool, error) {
if err != nil {
return hasPermission, err
}
if hasPermission {
g.log.Debug("User granted access to execute action", "userId", g.user.UserId, "orgId", g.orgId, "uname", g.user.Login, "dashId", g.dashId, "action", permission)
} else {
g.log.Debug("User denied access to execute action", "userId", g.user.UserId, "orgId", g.orgId, "uname", g.user.Login, "dashId", g.dashId, "action", permission)
}
return hasPermission, err
}
func (g *dashboardGuardianImpl) checkAcl(permission m.PermissionType, acl []*m.DashboardAclInfoDTO) (bool, error) {

View File

@@ -42,7 +42,8 @@ func (rs *RenderingService) renderViaPhantomJS(ctx context.Context, opts Opts) (
cmdArgs := []string{
"--ignore-ssl-errors=true",
"--web-security=false",
"--web-security=true",
"--local-url-access=false",
phantomDebugArg,
scriptPath,
fmt.Sprintf("url=%v", url),

View File

@@ -320,13 +320,18 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
"DELETE FROM dashboard WHERE id = ?",
"DELETE FROM playlist_item WHERE type = 'dashboard_by_id' AND value = ?",
"DELETE FROM dashboard_version WHERE dashboard_id = ?",
"DELETE FROM dashboard WHERE folder_id = ?",
"DELETE FROM annotation WHERE dashboard_id = ?",
"DELETE FROM dashboard_provisioning WHERE dashboard_id = ?",
}
if dashboard.IsFolder {
deletes = append(deletes, "DELETE FROM dashboard_provisioning WHERE dashboard_id in (select id from dashboard where folder_id = ?)")
deletes = append(deletes, "DELETE FROM dashboard WHERE folder_id = ?")
}
for _, sql := range deletes {
_, err := sess.Exec(sql, dashboard.Id)
if err != nil {
return err
}

View File

@@ -13,17 +13,30 @@ func TestDashboardProvisioningTest(t *testing.T) {
Convey("Testing Dashboard provisioning", t, func() {
InitTestDB(t)
saveDashboardCmd := &models.SaveDashboardCommand{
folderCmd := &models.SaveDashboardCommand{
OrgId: 1,
FolderId: 0,
IsFolder: false,
IsFolder: true,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": "test dashboard",
}),
}
Convey("Saving dashboards with extras", func() {
err := SaveDashboard(folderCmd)
So(err, ShouldBeNil)
saveDashboardCmd := &models.SaveDashboardCommand{
OrgId: 1,
IsFolder: false,
FolderId: folderCmd.Result.Id,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": "test dashboard",
}),
}
Convey("Saving dashboards with provisioning meta data", func() {
now := time.Now()
cmd := &models.SaveProvisionedDashboardCommand{
@@ -67,6 +80,21 @@ func TestDashboardProvisioningTest(t *testing.T) {
So(err, ShouldBeNil)
So(query.Result, ShouldBeFalse)
})
Convey("Deleteing folder should delete provision meta data", func() {
deleteCmd := &models.DeleteDashboardCommand{
Id: folderCmd.Result.Id,
OrgId: 1,
}
So(DeleteDashboard(deleteCmd), ShouldBeNil)
query := &models.IsDashboardProvisionedQuery{DashboardId: cmd.Result.Id}
err = GetProvisionedDataByDashboardId(query)
So(err, ShouldBeNil)
So(query.Result, ShouldBeFalse)
})
})
})
}

View File

@@ -86,9 +86,10 @@ func (e *CloudWatchExecutor) Query(ctx context.Context, dsInfo *models.DataSourc
}
func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryContext *tsdb.TsdbQuery) (*tsdb.Response, error) {
result := &tsdb.Response{
results := &tsdb.Response{
Results: make(map[string]*tsdb.QueryResult),
}
resultChan := make(chan *tsdb.QueryResult, len(queryContext.Queries))
eg, ectx := errgroup.WithContext(ctx)
@@ -102,10 +103,10 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo
RefId := queryContext.Queries[i].RefId
query, err := parseQuery(queryContext.Queries[i].Model)
if err != nil {
result.Results[RefId] = &tsdb.QueryResult{
results.Results[RefId] = &tsdb.QueryResult{
Error: err,
}
return result, nil
return results, nil
}
query.RefId = RefId
@@ -118,10 +119,10 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo
}
if query.Id == "" && query.Expression != "" {
result.Results[query.RefId] = &tsdb.QueryResult{
results.Results[query.RefId] = &tsdb.QueryResult{
Error: fmt.Errorf("Invalid query: id should be set if using expression"),
}
return result, nil
return results, nil
}
eg.Go(func() error {
@@ -130,12 +131,13 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo
return err
}
if err != nil {
result.Results[query.RefId] = &tsdb.QueryResult{
resultChan <- &tsdb.QueryResult{
RefId: query.RefId,
Error: err,
}
return nil
}
result.Results[queryRes.RefId] = queryRes
resultChan <- queryRes
return nil
})
}
@@ -149,10 +151,10 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo
return err
}
for _, queryRes := range queryResponses {
result.Results[queryRes.RefId] = queryRes
if err != nil {
result.Results[queryRes.RefId].Error = err
queryRes.Error = err
}
resultChan <- queryRes
}
return nil
})
@@ -162,8 +164,12 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo
if err := eg.Wait(); err != nil {
return nil, err
}
close(resultChan)
for result := range resultChan {
results.Results[result.RefId] = result
}
return result, nil
return results, nil
}
func (e *CloudWatchExecutor) executeQuery(ctx context.Context, query *CloudWatchQuery, queryContext *tsdb.TsdbQuery) (*tsdb.QueryResult, error) {

View File

@@ -337,11 +337,21 @@ func (e *StackdriverExecutor) unmarshalResponse(res *http.Response) (Stackdriver
func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data StackdriverResponse, query *StackdriverQuery) error {
metricLabels := make(map[string][]string)
resourceLabels := make(map[string][]string)
var resourceTypes []string
for _, series := range data.TimeSeries {
if !containsLabel(resourceTypes, series.Resource.Type) {
resourceTypes = append(resourceTypes, series.Resource.Type)
}
}
for _, series := range data.TimeSeries {
points := make([]tsdb.TimePoint, 0)
defaultMetricName := series.Metric.Type
if len(resourceTypes) > 1 {
defaultMetricName += " " + series.Resource.Type
}
for key, value := range series.Metric.Labels {
if !containsLabel(metricLabels[key], value) {
@@ -385,7 +395,7 @@ func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data Sta
points = append(points, tsdb.NewTimePoint(null.FloatFrom(value), float64((point.Interval.EndTime).Unix())*1000))
}
metricName := formatLegendKeys(series.Metric.Type, defaultMetricName, series.Metric.Labels, series.Resource.Labels, make(map[string]string), query)
metricName := formatLegendKeys(series.Metric.Type, defaultMetricName, series.Resource.Type, series.Metric.Labels, series.Resource.Labels, make(map[string]string), query)
queryRes.Series = append(queryRes.Series, &tsdb.TimeSeries{
Name: metricName,
@@ -411,7 +421,7 @@ func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data Sta
bucketBound := calcBucketBound(point.Value.DistributionValue.BucketOptions, i)
additionalLabels := map[string]string{"bucket": bucketBound}
buckets[i] = &tsdb.TimeSeries{
Name: formatLegendKeys(series.Metric.Type, defaultMetricName, series.Metric.Labels, series.Resource.Labels, additionalLabels, query),
Name: formatLegendKeys(series.Metric.Type, defaultMetricName, series.Resource.Type, series.Metric.Labels, series.Resource.Labels, additionalLabels, query),
Points: make([]tsdb.TimePoint, 0),
}
if maxKey < i {
@@ -427,7 +437,7 @@ func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data Sta
bucketBound := calcBucketBound(point.Value.DistributionValue.BucketOptions, i)
additionalLabels := map[string]string{"bucket": bucketBound}
buckets[i] = &tsdb.TimeSeries{
Name: formatLegendKeys(series.Metric.Type, defaultMetricName, series.Metric.Labels, series.Resource.Labels, additionalLabels, query),
Name: formatLegendKeys(series.Metric.Type, defaultMetricName, series.Resource.Type, series.Metric.Labels, series.Resource.Labels, additionalLabels, query),
Points: make([]tsdb.TimePoint, 0),
}
}
@@ -442,6 +452,7 @@ func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data Sta
queryRes.Meta.Set("resourceLabels", resourceLabels)
queryRes.Meta.Set("metricLabels", metricLabels)
queryRes.Meta.Set("groupBys", query.GroupBys)
queryRes.Meta.Set("resourceTypes", resourceTypes)
return nil
}
@@ -455,7 +466,7 @@ func containsLabel(labels []string, newLabel string) bool {
return false
}
func formatLegendKeys(metricType string, defaultMetricName string, metricLabels map[string]string, resourceLabels map[string]string, additionalLabels map[string]string, query *StackdriverQuery) string {
func formatLegendKeys(metricType string, defaultMetricName string, resourceType string, metricLabels map[string]string, resourceLabels map[string]string, additionalLabels map[string]string, query *StackdriverQuery) string {
if query.AliasBy == "" {
return defaultMetricName
}
@@ -469,6 +480,10 @@ func formatLegendKeys(metricType string, defaultMetricName string, metricLabels
return []byte(metricType)
}
if metaPartName == "resource.type" && resourceType != "" {
return []byte(resourceType)
}
metricPart := replaceWithMetricPart(metaPartName, metricType)
if metricPart != nil {

View File

@@ -81,7 +81,7 @@ function setViewModeBodyClass(body, mode, sidemenuOpen: boolean) {
break;
}
// 1 & true for legacy states
case 1:
case '1':
case true: {
body.removeClass('sidemenu-open');
body.addClass('view-mode--kiosk');
@@ -169,16 +169,16 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
const search = $location.search();
if (options && options.exit) {
search.kiosk = 1;
search.kiosk = '1';
}
switch (search.kiosk) {
case 'tv': {
search.kiosk = 1;
search.kiosk = true;
appEvents.emit('alert-success', ['Press ESC to exit Kiosk mode']);
break;
}
case 1:
case '1':
case true: {
delete search.kiosk;
break;

View File

@@ -103,7 +103,7 @@ export function queryPartEditorDirective($compile, templateSrv) {
$scope.$apply(() => {
$scope.handleEvent({ $event: { name: 'get-param-options' } }).then(result => {
const dynamicOptions = _.map(result, op => {
return op.value;
return _.escape(op.value);
});
callback(dynamicOptions);
});
@@ -117,6 +117,7 @@ export function queryPartEditorDirective($compile, templateSrv) {
minLength: 0,
items: 1000,
updater: value => {
value = _.unescape(value);
setTimeout(() => {
inputBlur.call($input[0], paramIndex);
}, 0);

View File

@@ -109,12 +109,12 @@ export function sqlPartEditorDirective($compile, templateSrv) {
$scope.$apply(() => {
$scope.handleEvent({ $event: { name: 'get-param-options', param: param } }).then(result => {
const dynamicOptions = _.map(result, op => {
return op.value;
return _.escape(op.value);
});
// add current value to dropdown if it's not in dynamicOptions
if (_.indexOf(dynamicOptions, part.params[paramIndex]) === -1) {
dynamicOptions.unshift(part.params[paramIndex]);
dynamicOptions.unshift(_.escape(part.params[paramIndex]));
}
callback(dynamicOptions);
@@ -129,6 +129,7 @@ export function sqlPartEditorDirective($compile, templateSrv) {
minLength: 0,
items: 1000,
updater: value => {
value = _.unescape(value);
if (value === part.params[paramIndex]) {
clearTimeout(cancelBlur);
$input.focus();

View File

@@ -3,7 +3,7 @@ import $ from 'jquery';
import coreModule from '../core_module';
/** @ngInject */
export function metricSegment($compile, $sce) {
export function metricSegment($compile, $sce, templateSrv) {
const inputTemplate =
'<input type="text" data-provide="typeahead" ' +
' class="gf-form-input input-medium"' +
@@ -41,13 +41,11 @@ export function metricSegment($compile, $sce) {
return;
}
value = _.unescape(value);
$scope.$apply(() => {
const selected = _.find($scope.altSegments, { value: value });
if (selected) {
segment.value = selected.value;
segment.html = selected.html || selected.value;
segment.html = selected.html || $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(selected.value));
segment.fake = false;
segment.expandable = selected.expandable;
@@ -56,7 +54,7 @@ export function metricSegment($compile, $sce) {
}
} else if (segment.custom !== 'false') {
segment.value = value;
segment.html = $sce.trustAsHtml(value);
segment.html = $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(value));
segment.expandable = true;
segment.fake = false;
}
@@ -95,7 +93,7 @@ export function metricSegment($compile, $sce) {
// add custom values
if (segment.custom !== 'false') {
if (!segment.fake && _.indexOf(options, segment.value) === -1) {
options.unshift(segment.value);
options.unshift(_.escape(segment.value));
}
}
@@ -105,6 +103,7 @@ export function metricSegment($compile, $sce) {
};
$scope.updater = value => {
value = _.unescape(value);
if (value === segment.value) {
clearTimeout(cancelBlur);
$input.focus();

View File

@@ -429,6 +429,11 @@ describe('templateSrv', () => {
name: 'period',
current: { value: '$__auto_interval_interval', text: 'auto' },
},
{
type: 'textbox',
name: 'empty_on_init',
current: { value: '', text: '' },
},
]);
_templateSrv.setGrafanaVariable('$__auto_interval_interval', '13m');
_templateSrv.updateTemplateData();
@@ -438,6 +443,11 @@ describe('templateSrv', () => {
const target = _templateSrv.replaceWithText('Server: $server, period: $period');
expect(target).toBe('Server: All, period: 13m');
});
it('should replace empty string-values with an empty string', () => {
const target = _templateSrv.replaceWithText('Hello $empty_on_init');
expect(target).toBe('Hello ');
});
});
describe('built in interval variables', () => {

View File

@@ -22,6 +22,11 @@ describe('containsVariable', () => {
expect(contains).toBe(true);
});
it('should find it with [[var:option]] syntax', () => {
const contains = containsVariable('this.[[test:csv]].filters', 'test');
expect(contains).toBe(true);
});
it('should find it when part of segment', () => {
const contains = containsVariable('metrics.$env.$group-*', 'group');
expect(contains).toBe(true);
@@ -36,6 +41,16 @@ describe('containsVariable', () => {
const contains = containsVariable('asd', 'asd2.$env', 'env');
expect(contains).toBe(true);
});
it('should find it with ${var} syntax', () => {
const contains = containsVariable('this.${test}.filters', 'test');
expect(contains).toBe(true);
});
it('should find it with ${var:option} syntax', () => {
const contains = containsVariable('this.${test:csv}.filters', 'test');
expect(contains).toBe(true);
});
});
});

View File

@@ -1,5 +1,6 @@
import kbn from 'app/core/utils/kbn';
import _ from 'lodash';
import { variableRegex } from 'app/features/templating/variable';
function luceneEscape(value) {
return value.replace(/([\!\*\+\-\=<>\s\&\|\(\)\[\]\{\}\^\~\?\:\\/"])/g, '\\$1');
@@ -8,13 +9,7 @@ function luceneEscape(value) {
export class TemplateSrv {
variables: any[];
/*
* This regex matches 3 types of variable reference with an optional format specifier
* \$(\w+) $var1
* \[\[([\s\S]+?)(?::(\w+))?\]\] [[var2]] or [[var2:fmt2]]
* \${(\w+)(?::(\w+))?} ${var3} or ${var3:fmt3}
*/
private regex = /\$(\w+)|\[\[([\s\S]+?)(?::(\w+))?\]\]|\${(\w+)(?::(\w+))?}/g;
private regex = variableRegex;
private index = {};
private grafanaVariables = {};
private builtIns = {};
@@ -30,17 +25,14 @@ export class TemplateSrv {
}
updateTemplateData() {
this.index = {};
const existsOrEmpty = value => value || value === '';
for (let i = 0; i < this.variables.length; i++) {
const variable = this.variables[i];
if (!variable.current || (!variable.current.isNone && !variable.current.value)) {
continue;
this.index = this.variables.reduce((acc, currentValue) => {
if (currentValue.current && !currentValue.current.isNone && existsOrEmpty(currentValue.current.value)) {
acc[currentValue.name] = currentValue;
}
this.index[variable.name] = variable;
}
return acc;
}, {});
}
variableInitialized(variable) {

View File

@@ -1,6 +1,19 @@
import kbn from 'app/core/utils/kbn';
import { assignModelProperties } from 'app/core/utils/model_utils';
/*
* This regex matches 3 types of variable reference with an optional format specifier
* \$(\w+) $var1
* \[\[([\s\S]+?)(?::(\w+))?\]\] [[var2]] or [[var2:fmt2]]
* \${(\w+)(?::(\w+))?} ${var3} or ${var3:fmt3}
*/
export const variableRegex = /\$(\w+)|\[\[([\s\S]+?)(?::(\w+))?\]\]|\${(\w+)(?::(\w+))?}/g;
// Helper function since lastIndex is not reset
export const variableRegexExec = (variableString: string) => {
variableRegex.lastIndex = 0;
return variableRegex.exec(variableString);
};
export interface Variable {
setValue(option);
updateOptions();
@@ -14,15 +27,16 @@ export let variableTypes = {};
export { assignModelProperties };
export function containsVariable(...args: any[]) {
let variableName = args[args.length - 1];
let str = args[0] || '';
const variableName = args[args.length - 1];
const variableString = args.slice(0, -1).join(' ');
const matches = variableString.match(variableRegex);
const isMatchingVariable =
matches !== null
? matches.find(match => {
const varMatch = variableRegexExec(match);
return varMatch !== null && varMatch.indexOf(variableName) > -1;
})
: false;
for (let i = 1; i < args.length - 1; i++) {
str += ' ' + args[i] || '';
}
variableName = kbn.regexEscape(variableName);
const findVarRegex = new RegExp('\\$(' + variableName + ')(?:\\W|$)|\\[\\[(' + variableName + ')\\]\\]', 'g');
const match = findVarRegex.exec(str);
return match !== null;
return !!isMatchingVariable;
}

View File

@@ -44,7 +44,13 @@ export default class CloudWatchDatasource {
// valid ExtendedStatistics is like p90.00, check the pattern
const hasInvalidStatistics = item.statistics.some(s => {
return s.indexOf('p') === 0 && !/p\d{2}\.\d{2}/.test(s);
if (s.indexOf('p') === 0) {
const matches = /^p\d{2}(?:\.\d{1,2})?$/.exec(s);
return !matches || matches[0] !== s;
}
return false;
});
if (hasInvalidStatistics) {
throw { message: 'Invalid extended statistics' };
@@ -131,7 +137,11 @@ export default class CloudWatchDatasource {
if (res.results) {
_.forEach(res.results, queryRes => {
_.forEach(queryRes.series, series => {
data.push({ target: series.name, datapoints: series.points, unit: queryRes.meta.unit || 'none' });
const s = { target: series.name, datapoints: series.points } as any;
if (queryRes.meta.unit) {
s.unit = queryRes.meta.unit;
}
data.push(s);
});
});
}

View File

@@ -122,7 +122,7 @@ describe('CloudWatchDatasource', () => {
});
});
it('should cancel query for invalid extended statistics', () => {
it.each(['pNN.NN', 'p9', 'p99.', 'p99.999'])('should cancel query for invalid extended statistics (%s)', stat => {
const query = {
range: { from: 'now-1h', to: 'now' },
rangeRaw: { from: 1483228800, to: 1483232400 },
@@ -134,7 +134,7 @@ describe('CloudWatchDatasource', () => {
dimensions: {
InstanceId: 'i-12345678',
},
statistics: ['pNN.NN'],
statistics: [stat],
period: '60s',
},
],

View File

@@ -20,7 +20,7 @@ export class PostgresDatasource {
this.interval = (instanceSettings.jsonData || {}).timeInterval;
}
interpolateVariable(value, variable) {
interpolateVariable = (value, variable) => {
if (typeof value === 'string') {
if (variable.multi || variable.includeAll) {
return this.queryModel.quoteLiteral(value);
@@ -37,7 +37,7 @@ export class PostgresDatasource {
return this.queryModel.quoteLiteral(v);
});
return quotedValues.join(',');
}
};
query(options) {
const queries = _.filter(options.targets, target => {

View File

@@ -89,7 +89,7 @@ export default class StackdriverDatasource {
}
resolvePanelUnitFromTargets(targets: any[]) {
let unit = 'none';
let unit;
if (targets.length > 0 && targets.every(t => t.unit === targets[0].unit)) {
if (stackdriverUnitMappings.hasOwnProperty(targets[0].unit)) {
unit = stackdriverUnitMappings[targets[0].unit];
@@ -106,21 +106,24 @@ export default class StackdriverDatasource {
if (!queryRes.series) {
return;
}
const unit = this.resolvePanelUnitFromTargets(options.targets);
queryRes.series.forEach(series => {
result.push({
let timeSerie: any = {
target: series.name,
datapoints: series.points,
refId: queryRes.refId,
meta: queryRes.meta,
unit,
});
};
if (unit) {
timeSerie = { ...timeSerie, unit };
}
result.push(timeSerie);
});
});
return { data: result };
} else {
return { data: [] };
}
return { data: result };
}
async annotationQuery(options) {

View File

@@ -44,7 +44,7 @@ export class FilterSegments {
this.removeSegment.value = DefaultRemoveFilterValue;
return Promise.resolve([this.removeSegment]);
} else {
return this.getFilterKeysFunc();
return this.getFilterKeysFunc(segment, DefaultRemoveFilterValue);
}
}

View File

@@ -28,7 +28,7 @@
<div class="gf-form">
<span class="gf-form-label query-keyword width-9">Group By</span>
<div class="gf-form" ng-repeat="segment in ctrl.groupBySegments">
<metric-segment segment="segment" get-options="ctrl.getGroupBys(segment, $index)" on-change="ctrl.groupByChanged(segment, $index)"></metric-segment>
<metric-segment segment="segment" get-options="ctrl.getGroupBys(segment)" on-change="ctrl.groupByChanged(segment, $index)"></metric-segment>
</div>
</div>
<div class="gf-form gf-form--grow">

View File

@@ -101,6 +101,5 @@ export class StackdriverQueryCtrl extends QueryCtrl {
this.lastQueryError = jsonBody.error.message;
}
}
console.error(err);
}
}

View File

@@ -1,6 +1,6 @@
import angular from 'angular';
import _ from 'lodash';
import { FilterSegments, DefaultRemoveFilterValue } from './filter_segments';
import { FilterSegments } from './filter_segments';
import appEvents from 'app/core/app_events';
export class StackdriverFilter {
@@ -26,8 +26,10 @@ export class StackdriverFilter {
export class StackdriverFilterCtrl {
metricLabels: { [key: string]: string[] };
resourceLabels: { [key: string]: string[] };
resourceTypes: string[];
defaultRemoveGroupByValue = '-- remove group by --';
resourceTypeValue = 'resource.type';
loadLabelsPromise: Promise<any>;
service: string;
@@ -72,7 +74,7 @@ export class StackdriverFilterCtrl {
this.filterSegments = new FilterSegments(
this.uiSegmentSrv,
this.target,
this.getGroupBys.bind(this, null, null, DefaultRemoveFilterValue, false),
this.getFilterKeys.bind(this),
this.getFilterValues.bind(this)
);
this.filterSegments.buildSegmentModel();
@@ -141,6 +143,7 @@ export class StackdriverFilterCtrl {
const data = await this.datasource.getLabels(this.target.metricType, this.target.refId);
this.metricLabels = data.results[this.target.refId].meta.metricLabels;
this.resourceLabels = data.results[this.target.refId].meta.resourceLabels;
this.resourceTypes = data.results[this.target.refId].meta.resourceTypes;
resolve();
} catch (error) {
if (error.data && error.data.message) {
@@ -181,45 +184,66 @@ export class StackdriverFilterCtrl {
this.$rootScope.$broadcast('metricTypeChanged');
}
async getGroupBys(segment, index, removeText?: string, removeUsed = true) {
async createLabelKeyElements() {
await this.loadLabelsPromise;
const metricLabels = Object.keys(this.metricLabels || {})
.filter(ml => {
if (!removeUsed) {
return true;
}
return this.target.aggregation.groupBys.indexOf('metric.label.' + ml) === -1;
})
.map(l => {
return this.uiSegmentSrv.newSegment({
value: `metric.label.${l}`,
expandable: false,
});
let elements = Object.keys(this.metricLabels || {}).map(l => {
return this.uiSegmentSrv.newSegment({
value: `metric.label.${l}`,
expandable: false,
});
});
const resourceLabels = Object.keys(this.resourceLabels || {})
.filter(ml => {
if (!removeUsed) {
return true;
}
return this.target.aggregation.groupBys.indexOf('resource.label.' + ml) === -1;
})
.map(l => {
elements = [
...elements,
...Object.keys(this.resourceLabels || {}).map(l => {
return this.uiSegmentSrv.newSegment({
value: `resource.label.${l}`,
expandable: false,
});
});
}),
];
const noValueOrPlusButton = !segment || segment.type === 'plus-button';
if (noValueOrPlusButton && metricLabels.length === 0 && resourceLabels.length === 0) {
return Promise.resolve([]);
if (this.resourceTypes && this.resourceTypes.length > 0) {
elements = [
...elements,
this.uiSegmentSrv.newSegment({
value: this.resourceTypeValue,
expandable: false,
}),
];
}
this.removeSegment.value = removeText || this.defaultRemoveGroupByValue;
return Promise.resolve([...metricLabels, ...resourceLabels, this.removeSegment]);
return elements;
}
async getFilterKeys(segment, removeText?: string) {
let elements = await this.createLabelKeyElements();
if (this.target.filters.indexOf(this.resourceTypeValue) !== -1) {
elements = elements.filter(e => e.value !== this.resourceTypeValue);
}
const noValueOrPlusButton = !segment || segment.type === 'plus-button';
if (noValueOrPlusButton && elements.length === 0) {
return [];
}
this.removeSegment.value = removeText;
return [...elements, this.removeSegment];
}
async getGroupBys(segment) {
let elements = await this.createLabelKeyElements();
elements = elements.filter(e => this.target.aggregation.groupBys.indexOf(e.value) === -1);
const noValueOrPlusButton = !segment || segment.type === 'plus-button';
if (noValueOrPlusButton && elements.length === 0) {
return [];
}
this.removeSegment.value = this.defaultRemoveGroupByValue;
return [...elements, this.removeSegment];
}
groupByChanged(segment, index) {
@@ -263,6 +287,10 @@ export class StackdriverFilterCtrl {
return this.resourceLabels[shortKey];
}
if (filterKey === this.resourceTypeValue) {
return this.resourceTypes;
}
return [];
}

View File

@@ -235,8 +235,8 @@ describe('StackdriverDataSource', () => {
beforeEach(() => {
res = ds.resolvePanelUnitFromTargets([{ unit: 'megaseconds' }]);
});
it('should return none', () => {
expect(res).toEqual('none');
it('should return undefined', () => {
expect(res).toBeUndefined();
});
});
describe('and the stackdriver unit has a corresponding grafana unit', () => {
@@ -262,16 +262,16 @@ describe('StackdriverDataSource', () => {
beforeEach(() => {
res = ds.resolvePanelUnitFromTargets([{ unit: 'megaseconds' }, { unit: 'megaseconds' }]);
});
it('should return the default value - none', () => {
expect(res).toEqual('none');
it('should return the default value of undefined', () => {
expect(res).toBeUndefined();
});
});
describe('and all target units are not the same', () => {
beforeEach(() => {
res = ds.resolvePanelUnitFromTargets([{ unit: 'bit' }, { unit: 'min' }]);
});
it('should return the default value - none', () => {
expect(res).toEqual('none');
it('should return the default value of undefined', () => {
expect(res).toBeUndefined();
});
});
});