From 3e93fd1372f5dacd78d0c798ffb0e77f4edc0b35 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 23 Apr 2018 09:23:14 +0200 Subject: [PATCH 01/23] return inherited property for permissions (cherry picked from commit d86ed679b14d172aa0e3945eed32d7c738b03ecd) --- pkg/models/dashboard_acl.go | 1 + pkg/services/guardian/guardian.go | 14 +----------- pkg/services/guardian/guardian_test.go | 8 +++---- pkg/services/sqlstore/dashboard_acl.go | 6 +++-- pkg/services/sqlstore/dashboard_acl_test.go | 25 +++++++++++++++++++++ 5 files changed, 35 insertions(+), 19 deletions(-) diff --git a/pkg/models/dashboard_acl.go b/pkg/models/dashboard_acl.go index 4ef8061486b..5fc09bd16b5 100644 --- a/pkg/models/dashboard_acl.go +++ b/pkg/models/dashboard_acl.go @@ -69,6 +69,7 @@ type DashboardAclInfoDTO struct { Slug string `json:"slug"` IsFolder bool `json:"isFolder"` Url string `json:"url"` + Inherited bool `json:"inherited"` } func (dto *DashboardAclInfoDTO) hasSameRoleAs(other *DashboardAclInfoDTO) bool { diff --git a/pkg/services/guardian/guardian.go b/pkg/services/guardian/guardian.go index 700f22d8d26..bf455adc7ca 100644 --- a/pkg/services/guardian/guardian.go +++ b/pkg/services/guardian/guardian.go @@ -154,12 +154,7 @@ func (g *dashboardGuardianImpl) CheckPermissionBeforeUpdate(permission m.Permiss // validate overridden permissions to be higher for _, a := range acl { for _, existingPerm := range existingPermissions { - // handle default permissions - if existingPerm.DashboardId == -1 { - existingPerm.DashboardId = g.dashId - } - - if a.DashboardId == existingPerm.DashboardId { + if !existingPerm.Inherited { continue } @@ -187,13 +182,6 @@ func (g *dashboardGuardianImpl) GetAcl() ([]*m.DashboardAclInfoDTO, error) { return nil, err } - for _, a := range query.Result { - // handle default permissions - if a.DashboardId == -1 { - a.DashboardId = g.dashId - } - } - g.acl = query.Result return g.acl, nil } diff --git a/pkg/services/guardian/guardian_test.go b/pkg/services/guardian/guardian_test.go index 9de12c60fea..abf92ae0555 100644 --- a/pkg/services/guardian/guardian_test.go +++ b/pkg/services/guardian/guardian_test.go @@ -217,13 +217,13 @@ func (sc *scenarioContext) parentFolderPermissionScenario(pt permissionType, per switch pt { case USER: - folderPermissionList = []*m.DashboardAclInfoDTO{{OrgId: orgID, DashboardId: parentFolderID, UserId: userID, Permission: permission}} + folderPermissionList = []*m.DashboardAclInfoDTO{{OrgId: orgID, DashboardId: parentFolderID, UserId: userID, Permission: permission, Inherited: true}} case TEAM: - folderPermissionList = []*m.DashboardAclInfoDTO{{OrgId: orgID, DashboardId: parentFolderID, TeamId: teamID, Permission: permission}} + folderPermissionList = []*m.DashboardAclInfoDTO{{OrgId: orgID, DashboardId: parentFolderID, TeamId: teamID, Permission: permission, Inherited: true}} case EDITOR: - folderPermissionList = []*m.DashboardAclInfoDTO{{OrgId: orgID, DashboardId: parentFolderID, Role: &editorRole, Permission: permission}} + folderPermissionList = []*m.DashboardAclInfoDTO{{OrgId: orgID, DashboardId: parentFolderID, Role: &editorRole, Permission: permission, Inherited: true}} case VIEWER: - folderPermissionList = []*m.DashboardAclInfoDTO{{OrgId: orgID, DashboardId: parentFolderID, Role: &viewerRole, Permission: permission}} + folderPermissionList = []*m.DashboardAclInfoDTO{{OrgId: orgID, DashboardId: parentFolderID, Role: &viewerRole, Permission: permission, Inherited: true}} } permissionScenario(fmt.Sprintf("and parent folder has %s with permission to %s", pt.String(), permission.String()), childDashboardID, sc, folderPermissionList, func(sc *scenarioContext) { diff --git a/pkg/services/sqlstore/dashboard_acl.go b/pkg/services/sqlstore/dashboard_acl.go index 2034ccf0d30..0b195c4562b 100644 --- a/pkg/services/sqlstore/dashboard_acl.go +++ b/pkg/services/sqlstore/dashboard_acl.go @@ -67,7 +67,8 @@ func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error { '' as title, '' as slug, '' as uid,` + - falseStr + ` AS is_folder + falseStr + ` AS is_folder,` + + falseStr + ` AS inherited FROM dashboard_acl as da WHERE da.dashboard_id = -1` query.Result = make([]*m.DashboardAclInfoDTO, 0) @@ -94,7 +95,8 @@ func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error { d.title, d.slug, d.uid, - d.is_folder + d.is_folder, + CASE WHEN (da.dashboard_id = -1 AND d.folder_id > 0) OR da.dashboard_id = d.folder_id THEN ` + dialect.BooleanStr(true) + ` ELSE ` + falseStr + ` END AS inherited FROM dashboard as d LEFT JOIN dashboard folder on folder.id = d.folder_id LEFT JOIN dashboard_acl AS da ON diff --git a/pkg/services/sqlstore/dashboard_acl_test.go b/pkg/services/sqlstore/dashboard_acl_test.go index 8fbb9c0d813..c68ffd08cd7 100644 --- a/pkg/services/sqlstore/dashboard_acl_test.go +++ b/pkg/services/sqlstore/dashboard_acl_test.go @@ -26,6 +26,22 @@ func TestDashboardAclDataAccess(t *testing.T) { }) Convey("Given dashboard folder with default permissions", func() { + Convey("When reading folder acl should include default acl", func() { + query := m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1} + + err := GetDashboardAclInfoList(&query) + So(err, ShouldBeNil) + + So(len(query.Result), ShouldEqual, 2) + defaultPermissionsId := -1 + So(query.Result[0].DashboardId, ShouldEqual, defaultPermissionsId) + So(*query.Result[0].Role, ShouldEqual, m.ROLE_VIEWER) + So(query.Result[0].Inherited, ShouldBeFalse) + So(query.Result[1].DashboardId, ShouldEqual, defaultPermissionsId) + So(*query.Result[1].Role, ShouldEqual, m.ROLE_EDITOR) + So(query.Result[1].Inherited, ShouldBeFalse) + }) + Convey("When reading dashboard acl should include acl for parent folder", func() { query := m.GetDashboardAclInfoListQuery{DashboardId: childDash.Id, OrgId: 1} @@ -36,8 +52,10 @@ func TestDashboardAclDataAccess(t *testing.T) { defaultPermissionsId := -1 So(query.Result[0].DashboardId, ShouldEqual, defaultPermissionsId) So(*query.Result[0].Role, ShouldEqual, m.ROLE_VIEWER) + So(query.Result[0].Inherited, ShouldBeTrue) So(query.Result[1].DashboardId, ShouldEqual, defaultPermissionsId) So(*query.Result[1].Role, ShouldEqual, m.ROLE_EDITOR) + So(query.Result[1].Inherited, ShouldBeTrue) }) }) @@ -94,7 +112,9 @@ func TestDashboardAclDataAccess(t *testing.T) { So(len(query.Result), ShouldEqual, 2) So(query.Result[0].DashboardId, ShouldEqual, savedFolder.Id) + So(query.Result[0].Inherited, ShouldBeTrue) So(query.Result[1].DashboardId, ShouldEqual, childDash.Id) + So(query.Result[1].Inherited, ShouldBeFalse) }) }) }) @@ -118,9 +138,12 @@ func TestDashboardAclDataAccess(t *testing.T) { So(len(query.Result), ShouldEqual, 3) So(query.Result[0].DashboardId, ShouldEqual, defaultPermissionsId) So(*query.Result[0].Role, ShouldEqual, m.ROLE_VIEWER) + So(query.Result[0].Inherited, ShouldBeTrue) So(query.Result[1].DashboardId, ShouldEqual, defaultPermissionsId) So(*query.Result[1].Role, ShouldEqual, m.ROLE_EDITOR) + So(query.Result[1].Inherited, ShouldBeTrue) So(query.Result[2].DashboardId, ShouldEqual, childDash.Id) + So(query.Result[2].Inherited, ShouldBeFalse) }) }) @@ -209,8 +232,10 @@ func TestDashboardAclDataAccess(t *testing.T) { defaultPermissionsId := -1 So(query.Result[0].DashboardId, ShouldEqual, defaultPermissionsId) So(*query.Result[0].Role, ShouldEqual, m.ROLE_VIEWER) + So(query.Result[0].Inherited, ShouldBeFalse) So(query.Result[1].DashboardId, ShouldEqual, defaultPermissionsId) So(*query.Result[1].Role, ShouldEqual, m.ROLE_EDITOR) + So(query.Result[1].Inherited, ShouldBeFalse) }) }) }) From a46d0204d9eb3127122abccdc41a4debb26cc099 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 23 Apr 2018 09:23:31 +0200 Subject: [PATCH 02/23] use inherited property from api when rendering permissions (cherry picked from commit 079346917f42ad677d44d7c14b0942e80e7d64cd) --- public/app/core/components/Permissions/PermissionsListItem.tsx | 2 +- public/app/stores/PermissionsStore/PermissionsStore.jest.ts | 1 + public/app/stores/PermissionsStore/PermissionsStore.ts | 2 -- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/public/app/core/components/Permissions/PermissionsListItem.tsx b/public/app/core/components/Permissions/PermissionsListItem.tsx index ee1108a6998..b0158525d52 100644 --- a/public/app/core/components/Permissions/PermissionsListItem.tsx +++ b/public/app/core/components/Permissions/PermissionsListItem.tsx @@ -41,7 +41,7 @@ export default observer(({ item, removeItem, permissionChanged, itemIndex, folde permissionChanged(itemIndex, permissionOption.value, permissionOption.label); }; - const inheritedFromRoot = item.dashboardId === -1 && folderInfo && folderInfo.id === 0; + const inheritedFromRoot = item.dashboardId === -1 && !item.inherited; return ( diff --git a/public/app/stores/PermissionsStore/PermissionsStore.jest.ts b/public/app/stores/PermissionsStore/PermissionsStore.jest.ts index d6a20e25846..6d88401e0d6 100644 --- a/public/app/stores/PermissionsStore/PermissionsStore.jest.ts +++ b/public/app/stores/PermissionsStore/PermissionsStore.jest.ts @@ -16,6 +16,7 @@ describe('PermissionsStore', () => { permissionName: 'View', teamId: 1, team: 'MyTestTeam', + inherited: true, }, { id: 5, diff --git a/public/app/stores/PermissionsStore/PermissionsStore.ts b/public/app/stores/PermissionsStore/PermissionsStore.ts index 833d1bdaac7..95d63c8527a 100644 --- a/public/app/stores/PermissionsStore/PermissionsStore.ts +++ b/public/app/stores/PermissionsStore/PermissionsStore.ts @@ -224,8 +224,6 @@ const prepareServerResponse = (response, dashboardId: number, isFolder: boolean, }; const prepareItem = (item, dashboardId: number, isFolder: boolean, isInRoot: boolean) => { - item.inherited = !isFolder && !isInRoot && dashboardId !== item.dashboardId; - item.sortRank = 0; if (item.userId > 0) { item.name = item.userLogin; From 111839bdccb36b106d1bf0edb14edc3facff006b Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Mon, 23 Apr 2018 13:00:24 +0200 Subject: [PATCH 03/23] added button to show more preview values for variables, button runs a function that increases options limit, fixes #11508 (cherry picked from commit c2cc77fa08e5f907b7b60ee867fd566808564a39) --- public/app/features/templating/editor_ctrl.ts | 5 +++++ public/app/features/templating/partials/editor.html | 11 +++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/public/app/features/templating/editor_ctrl.ts b/public/app/features/templating/editor_ctrl.ts index f20e93be42c..2533ac03739 100644 --- a/public/app/features/templating/editor_ctrl.ts +++ b/public/app/features/templating/editor_ctrl.ts @@ -10,6 +10,7 @@ export class VariableEditorCtrl { $scope.ctrl = {}; $scope.namePattern = /^(?!__).*$/; $scope._ = _; + $scope.optionsLimit = 20; $scope.refreshOptions = [ { value: 0, text: 'Never' }, @@ -165,6 +166,10 @@ export class VariableEditorCtrl { $scope.removeVariable = function(variable) { variableSrv.removeVariable(variable); }; + + $scope.showMoreOptions = function() { + $scope.optionsLimit += 20; + }; } } diff --git a/public/app/features/templating/partials/editor.html b/public/app/features/templating/partials/editor.html index d904aeb4789..74cb2f23e84 100644 --- a/public/app/features/templating/partials/editor.html +++ b/public/app/features/templating/partials/editor.html @@ -280,11 +280,14 @@
-
Preview of values (shows max 20)
+
Preview of values
-
- {{option.text}} -
+
+ {{option.text}} +
+
+ Show more +
From 0e8f05e6d564491cd98dbe6c64cbb2d6f63615de Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Tue, 24 Apr 2018 17:40:03 +0200 Subject: [PATCH 04/23] added pointer to show more, reset values on new query (cherry picked from commit a40314022b0db1110d6d38d8919f2526dd915ca8) --- public/app/features/templating/editor_ctrl.ts | 1 + public/app/features/templating/partials/editor.html | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/public/app/features/templating/editor_ctrl.ts b/public/app/features/templating/editor_ctrl.ts index 2533ac03739..75a84cca2bf 100644 --- a/public/app/features/templating/editor_ctrl.ts +++ b/public/app/features/templating/editor_ctrl.ts @@ -97,6 +97,7 @@ export class VariableEditorCtrl { }; $scope.runQuery = function() { + $scope.optionsLimit = 20; return variableSrv.updateOptions($scope.current).catch(err => { if (err.data && err.data.message) { err.message = err.data.message; diff --git a/public/app/features/templating/partials/editor.html b/public/app/features/templating/partials/editor.html index 74cb2f23e84..0d8b0ace327 100644 --- a/public/app/features/templating/partials/editor.html +++ b/public/app/features/templating/partials/editor.html @@ -286,7 +286,7 @@ {{option.text}}
- Show more + Show more
From 3908571baffa39e33edfed6d70138c098fe1f2fb Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 23 Apr 2018 16:02:59 +0200 Subject: [PATCH 05/23] db: fix failing user auth tests for postgres (cherry picked from commit d14ac54af665c825893cde3edb0c2f9f920d4986) --- pkg/services/sqlstore/user_auth_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/services/sqlstore/user_auth_test.go b/pkg/services/sqlstore/user_auth_test.go index 279fd7aa0f5..882e0c7afa5 100644 --- a/pkg/services/sqlstore/user_auth_test.go +++ b/pkg/services/sqlstore/user_auth_test.go @@ -32,7 +32,7 @@ func TestUserAuth(t *testing.T) { So(err, ShouldBeNil) _, err = x.Exec("DELETE FROM org WHERE 1=1") So(err, ShouldBeNil) - _, err = x.Exec("DELETE FROM user WHERE 1=1") + _, err = x.Exec("DELETE FROM " + dialect.Quote("user") + " WHERE 1=1") So(err, ShouldBeNil) _, err = x.Exec("DELETE FROM user_auth WHERE 1=1") So(err, ShouldBeNil) @@ -117,7 +117,7 @@ func TestUserAuth(t *testing.T) { So(query.Result.Login, ShouldEqual, "loginuser1") // remove user - _, err = x.Exec("DELETE FROM user WHERE id=?", query.Result.Id) + _, err = x.Exec("DELETE FROM "+dialect.Quote("user")+" WHERE id=?", query.Result.Id) So(err, ShouldBeNil) // get via user_auth for deleted user From 790fd996761dfb9c58e96d102331a8c554fd8dc4 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 23 Apr 2018 16:20:17 +0200 Subject: [PATCH 06/23] Fixes signing of packages. Signing was failing as the builds were expected to run as ubuntu but is run as root. Closes #11686 (cherry picked from commit 3a48ea8dde80975e781c6292a4af17f91204a8de) --- scripts/build/rpmmacros | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build/rpmmacros b/scripts/build/rpmmacros index a91ba9b8290..c00c8ec2eee 100644 --- a/scripts/build/rpmmacros +++ b/scripts/build/rpmmacros @@ -1,4 +1,4 @@ %_signature gpg -%_gpg_path /home/ubuntu/.gnupg +%_gpg_path /root/.gnupg %_gpg_name Grafana %_gpgbin /usr/bin/gpg From 06d52adf4e0fd2a603b0416477d12bea299ce0e8 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Mon, 23 Apr 2018 17:44:29 +0200 Subject: [PATCH 07/23] fixed so user who can edit dashboard can edit row, fixes #11466 (cherry picked from commit 3eaaa5d32d835f76bba93336710143b24895f7ba) --- public/app/features/dashboard/dashgrid/DashboardRow.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/dashboard/dashgrid/DashboardRow.tsx b/public/app/features/dashboard/dashgrid/DashboardRow.tsx index c2a84cb7da9..a95130de1f2 100644 --- a/public/app/features/dashboard/dashgrid/DashboardRow.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardRow.tsx @@ -95,7 +95,7 @@ export class DashboardRow extends React.Component { {title} ({hiddenPanels} hidden panels) - {config.bootData.user.orgRole !== 'Viewer' && ( + {this.dashboard.meta.canEdit === true && (
From f72c4bc0e0245ebfe26ed88a6c9ad0c5950aff36 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Mon, 23 Apr 2018 17:45:51 +0200 Subject: [PATCH 08/23] removed import config (cherry picked from commit 45e6d9fcc4e4d613b3e18b246d7ef7321207ada3) --- public/app/features/dashboard/dashgrid/DashboardRow.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/public/app/features/dashboard/dashgrid/DashboardRow.tsx b/public/app/features/dashboard/dashgrid/DashboardRow.tsx index a95130de1f2..b133d4450bb 100644 --- a/public/app/features/dashboard/dashgrid/DashboardRow.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardRow.tsx @@ -4,7 +4,6 @@ import { PanelModel } from '../panel_model'; import { PanelContainer } from './PanelContainer'; import templateSrv from 'app/features/templating/template_srv'; import appEvents from 'app/core/app_events'; -import config from 'app/core/config'; export interface DashboardRowProps { panel: PanelModel; From 8e7147a7dad2cf58530a789dce56becbe4f285f9 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Tue, 24 Apr 2018 09:45:53 +0200 Subject: [PATCH 09/23] fixed test (cherry picked from commit 1446f5444787a4e2250cc596bd5c59ed232a1eef) --- .../app/features/dashboard/specs/DashboardRow.jest.tsx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/public/app/features/dashboard/specs/DashboardRow.jest.tsx b/public/app/features/dashboard/specs/DashboardRow.jest.tsx index c0ac172aa26..fa1e6acead2 100644 --- a/public/app/features/dashboard/specs/DashboardRow.jest.tsx +++ b/public/app/features/dashboard/specs/DashboardRow.jest.tsx @@ -2,19 +2,13 @@ import React from 'react'; import { shallow } from 'enzyme'; import { DashboardRow } from '../dashgrid/DashboardRow'; import { PanelModel } from '../panel_model'; -import config from '../../../core/config'; describe('DashboardRow', () => { let wrapper, panel, getPanelContainer, dashboardMock; beforeEach(() => { dashboardMock = { toggleRow: jest.fn() }; - - config.bootData = { - user: { - orgRole: 'Admin', - }, - }; + dashboardMock.meta = { canEdit: true }; getPanelContainer = jest.fn().mockReturnValue({ getDashboard: jest.fn().mockReturnValue(dashboardMock), @@ -42,7 +36,7 @@ describe('DashboardRow', () => { }); it('should have zero actions as viewer', () => { - config.bootData.user.orgRole = 'Viewer'; + dashboardMock.meta.canEdit = false; panel = new PanelModel({ collapsed: false }); wrapper = shallow(); expect(wrapper.find('.dashboard-row__actions .pointer')).toHaveLength(0); From cb8d436ea3cf48de7a9fb0d00a787fd9d40ffee9 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Tue, 24 Apr 2018 11:22:58 +0200 Subject: [PATCH 10/23] changed test name and dashboardMock code (cherry picked from commit 38a4a2dc60581b0b74e703ef6650a08ae54fc92c) --- .../app/features/dashboard/specs/DashboardRow.jest.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/public/app/features/dashboard/specs/DashboardRow.jest.tsx b/public/app/features/dashboard/specs/DashboardRow.jest.tsx index fa1e6acead2..8424346b0c5 100644 --- a/public/app/features/dashboard/specs/DashboardRow.jest.tsx +++ b/public/app/features/dashboard/specs/DashboardRow.jest.tsx @@ -7,8 +7,12 @@ describe('DashboardRow', () => { let wrapper, panel, getPanelContainer, dashboardMock; beforeEach(() => { - dashboardMock = { toggleRow: jest.fn() }; - dashboardMock.meta = { canEdit: true }; + dashboardMock = { + toggleRow: jest.fn(), + meta: { + canEdit: true, + }, + }; getPanelContainer = jest.fn().mockReturnValue({ getDashboard: jest.fn().mockReturnValue(dashboardMock), @@ -35,7 +39,7 @@ describe('DashboardRow', () => { expect(wrapper.find('.dashboard-row__actions .pointer')).toHaveLength(2); }); - it('should have zero actions as viewer', () => { + it('should have zero actions when cannot edit', () => { dashboardMock.meta.canEdit = false; panel = new PanelModel({ collapsed: false }); wrapper = shallow(); From 3147bcccdd2fa14f1d403ca1ad9b925b1c2048ae Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 24 Apr 2018 19:50:14 +0200 Subject: [PATCH 11/23] sql datasource: extract common logic for converting value column to float (cherry picked from commit 76bd2aea44da99550ca3713442fc65dfe7a9d135) --- pkg/tsdb/sql_engine.go | 109 ++++++++++++++++++++++++++++++++++++ pkg/tsdb/sql_engine_test.go | 101 ++++++++++++++++++++++++++++++++- 2 files changed, 207 insertions(+), 3 deletions(-) diff --git a/pkg/tsdb/sql_engine.go b/pkg/tsdb/sql_engine.go index 56ed2cd3cb6..274e5b05dc1 100644 --- a/pkg/tsdb/sql_engine.go +++ b/pkg/tsdb/sql_engine.go @@ -2,9 +2,12 @@ package tsdb import ( "context" + "fmt" "sync" "time" + "github.com/grafana/grafana/pkg/components/null" + "github.com/go-xorm/core" "github.com/go-xorm/xorm" "github.com/grafana/grafana/pkg/components/simplejson" @@ -185,3 +188,109 @@ func ConvertSqlTimeColumnToEpochMs(values RowValues, timeIndex int) { } } } + +// ConvertSqlValueColumnToFloat converts timeseries value column to float. +func ConvertSqlValueColumnToFloat(columnName string, columnValue interface{}) (null.Float, error) { + var value null.Float + + switch typedValue := columnValue.(type) { + case int: + value = null.FloatFrom(float64(typedValue)) + case *int: + if typedValue == nil { + value.Valid = false + } else { + value = null.FloatFrom(float64(*typedValue)) + } + case int64: + value = null.FloatFrom(float64(typedValue)) + case *int64: + if typedValue == nil { + value.Valid = false + } else { + value = null.FloatFrom(float64(*typedValue)) + } + case int32: + value = null.FloatFrom(float64(typedValue)) + case *int32: + if typedValue == nil { + value.Valid = false + } else { + value = null.FloatFrom(float64(*typedValue)) + } + case int16: + value = null.FloatFrom(float64(typedValue)) + case *int16: + if typedValue == nil { + value.Valid = false + } else { + value = null.FloatFrom(float64(*typedValue)) + } + case int8: + value = null.FloatFrom(float64(typedValue)) + case *int8: + if typedValue == nil { + value.Valid = false + } else { + value = null.FloatFrom(float64(*typedValue)) + } + case uint: + value = null.FloatFrom(float64(typedValue)) + case *uint: + if typedValue == nil { + value.Valid = false + } else { + value = null.FloatFrom(float64(*typedValue)) + } + case uint64: + value = null.FloatFrom(float64(typedValue)) + case *uint64: + if typedValue == nil { + value.Valid = false + } else { + value = null.FloatFrom(float64(*typedValue)) + } + case uint32: + value = null.FloatFrom(float64(typedValue)) + case *uint32: + if typedValue == nil { + value.Valid = false + } else { + value = null.FloatFrom(float64(*typedValue)) + } + case uint16: + value = null.FloatFrom(float64(typedValue)) + case *uint16: + if typedValue == nil { + value.Valid = false + } else { + value = null.FloatFrom(float64(*typedValue)) + } + case uint8: + value = null.FloatFrom(float64(typedValue)) + case *uint8: + if typedValue == nil { + value.Valid = false + } else { + value = null.FloatFrom(float64(*typedValue)) + } + case float64: + value = null.FloatFrom(typedValue) + case *float64: + value = null.FloatFromPtr(typedValue) + case float32: + value = null.FloatFrom(float64(typedValue)) + case *float32: + if typedValue == nil { + value.Valid = false + } else { + value = null.FloatFrom(float64(*typedValue)) + } + case nil: + value.Valid = false + default: + return null.NewFloat(0, false), fmt.Errorf("Value column must have numeric datatype, column: %s type: %T value: %v", columnName, typedValue, typedValue) + } + + return value, nil +} diff --git a/pkg/tsdb/sql_engine_test.go b/pkg/tsdb/sql_engine_test.go index 4c6951a0196..ce1fb45de21 100644 --- a/pkg/tsdb/sql_engine_test.go +++ b/pkg/tsdb/sql_engine_test.go @@ -1,10 +1,11 @@ package tsdb import ( - "fmt" "testing" "time" + "github.com/grafana/grafana/pkg/components/null" + . "github.com/smartystreets/goconvey/convey" ) @@ -156,8 +157,6 @@ func TestSqlEngine(t *testing.T) { So(fixtures[1].(float64), ShouldEqual, tMilliseconds) So(fixtures[2].(float64), ShouldEqual, tMilliseconds) So(fixtures[3].(float64), ShouldEqual, tMilliseconds) - fmt.Println(fixtures[4].(float64)) - fmt.Println(tMilliseconds) So(fixtures[4].(float64), ShouldEqual, tMilliseconds) So(fixtures[5].(float64), ShouldEqual, tMilliseconds) So(fixtures[6], ShouldBeNil) @@ -183,5 +182,101 @@ func TestSqlEngine(t *testing.T) { So(fixtures[2], ShouldBeNil) }) }) + + Convey("Given row with value columns", func() { + intValue := 1 + int64Value := int64(1) + int32Value := int32(1) + int16Value := int16(1) + int8Value := int8(1) + float64Value := float64(1) + float32Value := float32(1) + uintValue := uint(1) + uint64Value := uint64(1) + uint32Value := uint32(1) + uint16Value := uint16(1) + uint8Value := uint8(1) + + fixtures := make([]interface{}, 24) + fixtures[0] = intValue + fixtures[1] = &intValue + fixtures[2] = int64Value + fixtures[3] = &int64Value + fixtures[4] = int32Value + fixtures[5] = &int32Value + fixtures[6] = int16Value + fixtures[7] = &int16Value + fixtures[8] = int8Value + fixtures[9] = &int8Value + fixtures[10] = float64Value + fixtures[11] = &float64Value + fixtures[12] = float32Value + fixtures[13] = &float32Value + fixtures[14] = uintValue + fixtures[15] = &uintValue + fixtures[16] = uint64Value + fixtures[17] = &uint64Value + fixtures[18] = uint32Value + fixtures[19] = &uint32Value + fixtures[20] = uint16Value + fixtures[21] = &uint16Value + fixtures[22] = uint8Value + fixtures[23] = &uint8Value + + var intNilPointer *int + var int64NilPointer *int64 + var int32NilPointer *int32 + var int16NilPointer *int16 + var int8NilPointer *int8 + var float64NilPointer *float64 + var float32NilPointer *float32 + var uintNilPointer *uint + var uint64NilPointer *uint64 + var uint32NilPointer *uint32 + var uint16NilPointer *uint16 + var uint8NilPointer *uint8 + + nilPointerFixtures := make([]interface{}, 12) + nilPointerFixtures[0] = intNilPointer + nilPointerFixtures[1] = int64NilPointer + nilPointerFixtures[2] = int32NilPointer + nilPointerFixtures[3] = int16NilPointer + nilPointerFixtures[4] = int8NilPointer + nilPointerFixtures[5] = float64NilPointer + nilPointerFixtures[6] = float32NilPointer + nilPointerFixtures[7] = uintNilPointer + nilPointerFixtures[8] = uint64NilPointer + nilPointerFixtures[9] = uint32NilPointer + nilPointerFixtures[10] = uint16NilPointer + nilPointerFixtures[11] = uint8NilPointer + + Convey("When converting values to float should return expected value", func() { + for _, f := range fixtures { + value, _ := ConvertSqlValueColumnToFloat("col", f) + + if !value.Valid { + t.Fatalf("Failed to convert %T value, expected a valid float value", f) + } + + if value.Float64 != null.FloatFrom(1).Float64 { + t.Fatalf("Failed to convert %T value, expected a float value of 1.000, but got %v", f, value) + } + } + }) + + Convey("When converting nil pointer values to float should return expected value", func() { + for _, f := range nilPointerFixtures { + value, err := ConvertSqlValueColumnToFloat("col", f) + + if err != nil { + t.Fatalf("Failed to convert %T value, expected a non nil error, but got %v", f, err) + } + + if value.Valid { + t.Fatalf("Failed to convert %T value, expected an invalid float value", f) + } + } + }) + }) }) } From ac22f85d37490f7b6fa62033c329e5d1082e3970 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 24 Apr 2018 19:53:06 +0200 Subject: [PATCH 12/23] mysql: fix value columns conversion to float when using timeseries query (cherry picked from commit 346577b664acdbbc219666437dbd3b3ecf312671) --- pkg/tsdb/mysql/mysql.go | 12 +++--------- pkg/tsdb/mysql/mysql_test.go | 32 ++++++++++++++++---------------- 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/pkg/tsdb/mysql/mysql.go b/pkg/tsdb/mysql/mysql.go index 4f5cd1b0784..7eceaffdb09 100644 --- a/pkg/tsdb/mysql/mysql.go +++ b/pkg/tsdb/mysql/mysql.go @@ -265,16 +265,10 @@ func (e MysqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core. continue } - switch columnValue := values[i].(type) { - case int64: - value = null.FloatFrom(float64(columnValue)) - case float64: - value = null.FloatFrom(columnValue) - case nil: - value.Valid = false - default: - return fmt.Errorf("Value column must have numeric datatype, column: %s type: %T value: %v", col, columnValue, columnValue) + if value, err = tsdb.ConvertSqlValueColumnToFloat(col, values[i]); err != nil { + return err } + if metricIndex == -1 { metric = col } diff --git a/pkg/tsdb/mysql/mysql_test.go b/pkg/tsdb/mysql/mysql_test.go index 74cedea803a..29c5b72b408 100644 --- a/pkg/tsdb/mysql/mysql_test.go +++ b/pkg/tsdb/mysql/mysql_test.go @@ -420,12 +420,12 @@ func TestMySQL(t *testing.T) { So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6)) }) - Convey("When doing a metric query using epoch (int64) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (int64) as time column and value column (int64) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT timeInt64 as time, valueOne FROM metric_values ORDER BY time LIMIT 1`, + "rawSql": `SELECT timeInt64 as time, timeInt64 FROM metric_values ORDER BY time LIMIT 1`, "format": "time_series", }), RefId: "A", @@ -442,12 +442,12 @@ func TestMySQL(t *testing.T) { So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6)) }) - Convey("When doing a metric query using epoch (int64 nullable) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (int64 nullable) as time column and value column (int64 nullable) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT timeInt64Nullable as time, valueOne FROM metric_values ORDER BY time LIMIT 1`, + "rawSql": `SELECT timeInt64Nullable as time, timeInt64Nullable FROM metric_values ORDER BY time LIMIT 1`, "format": "time_series", }), RefId: "A", @@ -464,12 +464,12 @@ func TestMySQL(t *testing.T) { So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6)) }) - Convey("When doing a metric query using epoch (float64) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (float64) as time column and value column (float64) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT timeFloat64 as time, valueOne FROM metric_values ORDER BY time LIMIT 1`, + "rawSql": `SELECT timeFloat64 as time, timeFloat64 FROM metric_values ORDER BY time LIMIT 1`, "format": "time_series", }), RefId: "A", @@ -486,12 +486,12 @@ func TestMySQL(t *testing.T) { So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6)) }) - Convey("When doing a metric query using epoch (float64 nullable) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (float64 nullable) as time column and value column (float64 nullable) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT timeFloat64Nullable as time, valueOne FROM metric_values ORDER BY time LIMIT 1`, + "rawSql": `SELECT timeFloat64Nullable as time, timeFloat64Nullable FROM metric_values ORDER BY time LIMIT 1`, "format": "time_series", }), RefId: "A", @@ -508,12 +508,12 @@ func TestMySQL(t *testing.T) { So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6)) }) - Convey("When doing a metric query using epoch (int32) as time column should return metric with time in milliseconds", func() { + FocusConvey("When doing a metric query using epoch (int32) as time column and value column (int32) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT timeInt32 as time, valueOne FROM metric_values ORDER BY time LIMIT 1`, + "rawSql": `SELECT timeInt32 as time, timeInt32 FROM metric_values ORDER BY time LIMIT 1`, "format": "time_series", }), RefId: "A", @@ -530,12 +530,12 @@ func TestMySQL(t *testing.T) { So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6)) }) - Convey("When doing a metric query using epoch (int32 nullable) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (int32 nullable) as time column and value column (int32 nullable) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT timeInt32Nullable as time, valueOne FROM metric_values ORDER BY time LIMIT 1`, + "rawSql": `SELECT timeInt32Nullable as time, timeInt32Nullable FROM metric_values ORDER BY time LIMIT 1`, "format": "time_series", }), RefId: "A", @@ -552,12 +552,12 @@ func TestMySQL(t *testing.T) { So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6)) }) - Convey("When doing a metric query using epoch (float32) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (float32) as time column and value column (float32) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT timeFloat32 as time, valueOne FROM metric_values ORDER BY time LIMIT 1`, + "rawSql": `SELECT timeFloat32 as time, timeFloat32 FROM metric_values ORDER BY time LIMIT 1`, "format": "time_series", }), RefId: "A", @@ -574,12 +574,12 @@ func TestMySQL(t *testing.T) { So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(float64(float32(tInitial.Unix())))*1e3) }) - Convey("When doing a metric query using epoch (float32 nullable) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (float32 nullable) as time column and value column (float32 nullable) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT timeFloat32Nullable as time, valueOne FROM metric_values ORDER BY time LIMIT 1`, + "rawSql": `SELECT timeFloat32Nullable as time, timeFloat32Nullable FROM metric_values ORDER BY time LIMIT 1`, "format": "time_series", }), RefId: "A", From 66938e80c92c821701e075366a176931c2cec88e Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 24 Apr 2018 19:53:36 +0200 Subject: [PATCH 13/23] postgres: fix value columns conversion to float when using timeseries query (cherry picked from commit cf43007531fb9593574c3ba6a8888b0af6a7a78c) --- pkg/tsdb/postgres/postgres.go | 12 +++-------- pkg/tsdb/postgres/postgres_test.go | 32 +++++++++++++++--------------- 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/pkg/tsdb/postgres/postgres.go b/pkg/tsdb/postgres/postgres.go index 72d50b32d04..fdf09216e51 100644 --- a/pkg/tsdb/postgres/postgres.go +++ b/pkg/tsdb/postgres/postgres.go @@ -245,16 +245,10 @@ func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *co continue } - switch columnValue := values[i].(type) { - case int64: - value = null.FloatFrom(float64(columnValue)) - case float64: - value = null.FloatFrom(columnValue) - case nil: - value.Valid = false - default: - return fmt.Errorf("Value column must have numeric datatype, column: %s type: %T value: %v", col, columnValue, columnValue) + if value, err = tsdb.ConvertSqlValueColumnToFloat(col, values[i]); err != nil { + return err } + if metricIndex == -1 { metric = col } diff --git a/pkg/tsdb/postgres/postgres_test.go b/pkg/tsdb/postgres/postgres_test.go index d18251bac7d..7f24d5a2063 100644 --- a/pkg/tsdb/postgres/postgres_test.go +++ b/pkg/tsdb/postgres/postgres_test.go @@ -353,12 +353,12 @@ func TestPostgres(t *testing.T) { _, err = sess.InsertMulti(series) So(err, ShouldBeNil) - Convey("When doing a metric query using epoch (int64) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (int64) as time column and value column (int64) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT "timeInt64" as time, "valueOne" FROM metric_values ORDER BY time LIMIT 1`, + "rawSql": `SELECT "timeInt64" as time, "timeInt64" FROM metric_values ORDER BY time LIMIT 1`, "format": "time_series", }), RefId: "A", @@ -375,12 +375,12 @@ func TestPostgres(t *testing.T) { So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6)) }) - Convey("When doing a metric query using epoch (int64 nullable) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (int64 nullable) as time column and value column (int64 nullable) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT "timeInt64Nullable" as time, "valueOne" FROM metric_values ORDER BY time LIMIT 1`, + "rawSql": `SELECT "timeInt64Nullable" as time, "timeInt64Nullable" FROM metric_values ORDER BY time LIMIT 1`, "format": "time_series", }), RefId: "A", @@ -397,12 +397,12 @@ func TestPostgres(t *testing.T) { So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6)) }) - Convey("When doing a metric query using epoch (float64) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (float64) as time column and value column (float64) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT "timeFloat64" as time, "valueOne" FROM metric_values ORDER BY time LIMIT 1`, + "rawSql": `SELECT "timeFloat64" as time, "timeFloat64" FROM metric_values ORDER BY time LIMIT 1`, "format": "time_series", }), RefId: "A", @@ -419,12 +419,12 @@ func TestPostgres(t *testing.T) { So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6)) }) - Convey("When doing a metric query using epoch (float64 nullable) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (float64 nullable) as time column and value column (float64 nullable) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT "timeFloat64Nullable" as time, "valueOne" FROM metric_values ORDER BY time LIMIT 1`, + "rawSql": `SELECT "timeFloat64Nullable" as time, "timeFloat64Nullable" FROM metric_values ORDER BY time LIMIT 1`, "format": "time_series", }), RefId: "A", @@ -441,12 +441,12 @@ func TestPostgres(t *testing.T) { So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6)) }) - Convey("When doing a metric query using epoch (int32) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (int32) as time column and value column (int32) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT "timeInt32" as time, "valueOne" FROM metric_values ORDER BY time LIMIT 1`, + "rawSql": `SELECT "timeInt32" as time, "timeInt32" FROM metric_values ORDER BY time LIMIT 1`, "format": "time_series", }), RefId: "A", @@ -463,12 +463,12 @@ func TestPostgres(t *testing.T) { So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6)) }) - Convey("When doing a metric query using epoch (int32 nullable) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (int32 nullable) as time column and value column (int32 nullable) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT "timeInt32Nullable" as time, "valueOne" FROM metric_values ORDER BY time LIMIT 1`, + "rawSql": `SELECT "timeInt32Nullable" as time, "timeInt32Nullable" FROM metric_values ORDER BY time LIMIT 1`, "format": "time_series", }), RefId: "A", @@ -485,12 +485,12 @@ func TestPostgres(t *testing.T) { So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6)) }) - Convey("When doing a metric query using epoch (float32) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (float32) as time column and value column (float32) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT "timeFloat32" as time, "valueOne" FROM metric_values ORDER BY time LIMIT 1`, + "rawSql": `SELECT "timeFloat32" as time, "timeFloat32" FROM metric_values ORDER BY time LIMIT 1`, "format": "time_series", }), RefId: "A", @@ -507,12 +507,12 @@ func TestPostgres(t *testing.T) { So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(float64(float32(tInitial.Unix())))*1e3) }) - Convey("When doing a metric query using epoch (float32 nullable) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (float32 nullable) as time column and value column (float32 nullable) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT "timeFloat32Nullable" as time, "valueOne" FROM metric_values ORDER BY time LIMIT 1`, + "rawSql": `SELECT "timeFloat32Nullable" as time, "timeFloat32Nullable" FROM metric_values ORDER BY time LIMIT 1`, "format": "time_series", }), RefId: "A", From b58dc6cd49da0cfdcbab4dbdac9d93da58549ac5 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 24 Apr 2018 19:54:08 +0200 Subject: [PATCH 14/23] mssql: fix value columns conversion to float when using timeseries query (cherry picked from commit 1452634a2a5dc26b25deb958a46145d76c9a21a6) --- pkg/tsdb/mssql/mssql.go | 12 +++--------- pkg/tsdb/mssql/mssql_test.go | 32 ++++++++++++++++---------------- 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/pkg/tsdb/mssql/mssql.go b/pkg/tsdb/mssql/mssql.go index a598b7239ed..eb71259b46b 100644 --- a/pkg/tsdb/mssql/mssql.go +++ b/pkg/tsdb/mssql/mssql.go @@ -256,16 +256,10 @@ func (e MssqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core. continue } - switch columnValue := values[i].(type) { - case int64: - value = null.FloatFrom(float64(columnValue)) - case float64: - value = null.FloatFrom(columnValue) - case nil: - value.Valid = false - default: - return fmt.Errorf("Value column must have numeric datatype, column: %s type: %T value: %v", col, columnValue, columnValue) + if value, err = tsdb.ConvertSqlValueColumnToFloat(col, values[i]); err != nil { + return err } + if metricIndex == -1 { metric = col } diff --git a/pkg/tsdb/mssql/mssql_test.go b/pkg/tsdb/mssql/mssql_test.go index 599f4869f6a..167d02a1e07 100644 --- a/pkg/tsdb/mssql/mssql_test.go +++ b/pkg/tsdb/mssql/mssql_test.go @@ -374,12 +374,12 @@ func TestMSSQL(t *testing.T) { _, err = sess.InsertMulti(series) So(err, ShouldBeNil) - Convey("When doing a metric query using epoch (int64) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (int64) as time column and value column (int64) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT TOP 1 timeInt64 as time, valueOne FROM metric_values ORDER BY time`, + "rawSql": `SELECT TOP 1 timeInt64 as time, timeInt64 FROM metric_values ORDER BY time`, "format": "time_series", }), RefId: "A", @@ -396,12 +396,12 @@ func TestMSSQL(t *testing.T) { So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6)) }) - Convey("When doing a metric query using epoch (int64 nullable) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (int64 nullable) as time column and value column (int64 nullable) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT TOP 1 timeInt64Nullable as time, valueOne FROM metric_values ORDER BY time`, + "rawSql": `SELECT TOP 1 timeInt64Nullable as time, timeInt64Nullable FROM metric_values ORDER BY time`, "format": "time_series", }), RefId: "A", @@ -418,12 +418,12 @@ func TestMSSQL(t *testing.T) { So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6)) }) - Convey("When doing a metric query using epoch (float64) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (float64) as time column and value column (float64) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT TOP 1 timeFloat64 as time, valueOne FROM metric_values ORDER BY time`, + "rawSql": `SELECT TOP 1 timeFloat64 as time, timeFloat64 FROM metric_values ORDER BY time`, "format": "time_series", }), RefId: "A", @@ -440,12 +440,12 @@ func TestMSSQL(t *testing.T) { So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6)) }) - Convey("When doing a metric query using epoch (float64 nullable) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (float64 nullable) as time column and value column (float64 nullable) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT TOP 1 timeFloat64Nullable as time, valueOne FROM metric_values ORDER BY time`, + "rawSql": `SELECT TOP 1 timeFloat64Nullable as time, timeFloat64Nullable FROM metric_values ORDER BY time`, "format": "time_series", }), RefId: "A", @@ -462,12 +462,12 @@ func TestMSSQL(t *testing.T) { So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6)) }) - Convey("When doing a metric query using epoch (int32) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (int32) as time column and value column (int32) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT TOP 1 timeInt32 as time, valueOne FROM metric_values ORDER BY time`, + "rawSql": `SELECT TOP 1 timeInt32 as time, timeInt32 FROM metric_values ORDER BY time`, "format": "time_series", }), RefId: "A", @@ -484,12 +484,12 @@ func TestMSSQL(t *testing.T) { So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6)) }) - Convey("When doing a metric query using epoch (int32 nullable) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (int32 nullable) as time column and value column (int32 nullable) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT TOP 1 timeInt32Nullable as time, valueOne FROM metric_values ORDER BY time`, + "rawSql": `SELECT TOP 1 timeInt32Nullable as time, timeInt32Nullable FROM metric_values ORDER BY time`, "format": "time_series", }), RefId: "A", @@ -506,12 +506,12 @@ func TestMSSQL(t *testing.T) { So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6)) }) - Convey("When doing a metric query using epoch (float32) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (float32) as time column and value column (float32) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT TOP 1 timeFloat32 as time, valueOne FROM metric_values ORDER BY time`, + "rawSql": `SELECT TOP 1 timeFloat32 as time, timeFloat32 FROM metric_values ORDER BY time`, "format": "time_series", }), RefId: "A", @@ -528,12 +528,12 @@ func TestMSSQL(t *testing.T) { So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(float64(float32(tInitial.Unix())))*1e3) }) - Convey("When doing a metric query using epoch (float32 nullable) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (float32 nullable) as time column and value column (float32 nullable) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT TOP 1 timeFloat32Nullable as time, valueOne FROM metric_values ORDER BY time`, + "rawSql": `SELECT TOP 1 timeFloat32Nullable as time, timeFloat32Nullable FROM metric_values ORDER BY time`, "format": "time_series", }), RefId: "A", From fa1c1274db74b6ee6d95f3ca68161cc9fcaa6980 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Wed, 25 Apr 2018 12:16:43 +0200 Subject: [PATCH 15/23] replaced border hack carot with fontawesome carot fixes #11677 (cherry picked from commit 99aa9a46bcd67de0dc477df003331f7405f04dff) --- public/sass/components/_dropdown.scss | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/public/sass/components/_dropdown.scss b/public/sass/components/_dropdown.scss index cc94a379e07..37dbdcd89ef 100644 --- a/public/sass/components/_dropdown.scss +++ b/public/sass/components/_dropdown.scss @@ -256,17 +256,15 @@ // Caret to indicate there is a submenu .dropdown-submenu > a::after { - display: block; - content: ' '; - float: right; - width: 0; - height: 0; - border-color: transparent; - border-style: solid; - border-width: 5px 0 5px 5px; - border-left-color: $text-color-weak; - margin-top: 5px; - margin-right: -4px; + position: absolute; + top: 35%; + right: $input-padding-x; + background-color: transparent; + color: $text-color-weak; + font: normal normal normal $font-size-sm/1 FontAwesome; + content: '\f0da'; + pointer-events: none; + font-size: 11px; } .dropdown-submenu:hover > a::after { border-left-color: $dropdownLinkColorHover; From cadca93d93df595332af314934eef1ae7958c335 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Wed, 25 Apr 2018 12:44:39 +0200 Subject: [PATCH 16/23] removed height 100% from panel-container to fix ie11 panel edit mode (cherry picked from commit 6836268f3ebbb576bda961bfa1198db7092a6c16) --- public/sass/pages/_dashboard.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/public/sass/pages/_dashboard.scss b/public/sass/pages/_dashboard.scss index 871db4dfc2d..cf32522df7f 100644 --- a/public/sass/pages/_dashboard.scss +++ b/public/sass/pages/_dashboard.scss @@ -33,7 +33,6 @@ div.flot-text { border: $panel-border; position: relative; border-radius: 3px; - height: 100%; &.panel-transparent { background-color: transparent; From cb83ec89455ebc12f42e37a48309c106c08d5ead Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Fri, 20 Apr 2018 15:28:04 +0200 Subject: [PATCH 17/23] Add silent option to backend requests * When set to `true`, the `silent` option for backend_srv requests suppresses all event emitters that are triggered when the response is received. * Added `helperRequest()` to the Prometheus datasource to support requests that are not triggered by the user, e.g., for tab completion. `helperRequest()` sets the `silent` option. * Migrated all non-timeseries queries of the Prometheus datasource to use `helperRequest()`. Fixes #11673 (cherry picked from commit 53817b74295fe44249a8a9d8786158cac169acb0) --- public/app/core/services/backend_srv.ts | 9 ++++++--- .../plugins/datasource/prometheus/datasource.ts | 16 +++++++++++++++- .../datasource/prometheus/metric_find_query.ts | 8 ++++---- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index 8b7ca518e8b..d582b6a3b18 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -170,7 +170,9 @@ export class BackendSrv { return this.$http(options) .then(response => { - appEvents.emit('ds-request-response', response); + if (!options.silent) { + appEvents.emit('ds-request-response', response); + } return response; }) .catch(err => { @@ -201,8 +203,9 @@ export class BackendSrv { if (err.data && !err.data.message && _.isString(err.data.error)) { err.data.message = err.data.error; } - - appEvents.emit('ds-request-error', err); + if (!options.silent) { + appEvents.emit('ds-request-error', err); + } throw err; }) .finally(() => { diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 6cf6c713a90..3eceaf6c622 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -81,6 +81,20 @@ export class PrometheusDatasource { return this.backendSrv.datasourceRequest(options); } + // Use this for tab completion features, wont publish response to other components + helperRequest(url) { + const options: any = { + url: this.url + url, + silent: true, + }; + + if (this.basicAuth || this.withCredentials) { + options.withCredentials = true; + } + + return this.backendSrv.datasourceRequest(options); + } + interpolateQueryExpr(value, variable, defaultFormatFn) { // if no multi or include all do not regexEscape if (!variable.multi && !variable.includeAll) { @@ -229,7 +243,7 @@ export class PrometheusDatasource { ); } - return this._request('GET', url).then(result => { + return this.helperRequest(url).then(result => { this.metricsNameCache = { data: result.data.data, expire: Date.now() + 60 * 1000, diff --git a/public/app/plugins/datasource/prometheus/metric_find_query.ts b/public/app/plugins/datasource/prometheus/metric_find_query.ts index b27f1cd50af..c58f5c097b9 100644 --- a/public/app/plugins/datasource/prometheus/metric_find_query.ts +++ b/public/app/plugins/datasource/prometheus/metric_find_query.ts @@ -46,7 +46,7 @@ export default class PrometheusMetricFindQuery { // return label values globally url = '/api/v1/label/' + label + '/values'; - return this.datasource._request('GET', url).then(function(result) { + return this.datasource.helperRequest(url).then(function(result) { return _.map(result.data.data, function(value) { return { text: value }; }); @@ -56,7 +56,7 @@ export default class PrometheusMetricFindQuery { var end = this.datasource.getPrometheusTime(this.range.to, true); url = '/api/v1/series?match[]=' + encodeURIComponent(metric) + '&start=' + start + '&end=' + end; - return this.datasource._request('GET', url).then(function(result) { + return this.datasource.helperRequest(url).then(function(result) { var _labels = _.map(result.data.data, function(metric) { return metric[label] || ''; }).filter(function(label) { @@ -76,7 +76,7 @@ export default class PrometheusMetricFindQuery { metricNameQuery(metricFilterPattern) { var url = '/api/v1/label/__name__/values'; - return this.datasource._request('GET', url).then(function(result) { + return this.datasource.helperRequest(url).then(function(result) { return _.chain(result.data.data) .filter(function(metricName) { var r = new RegExp(metricFilterPattern); @@ -120,7 +120,7 @@ export default class PrometheusMetricFindQuery { var url = '/api/v1/series?match[]=' + encodeURIComponent(query) + '&start=' + start + '&end=' + end; var self = this; - return this.datasource._request('GET', url).then(function(result) { + return this.datasource.helperRequest(url).then(function(result) { return _.map(result.data.data, function(metric) { return { text: self.datasource.getOriginalMetricName(metric), From 9149a0c6557116c66a8ce86e5b1ed8c2f339510c Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Tue, 24 Apr 2018 12:27:37 +0200 Subject: [PATCH 18/23] Renamed helperRequest and removed positional args From review feedback: * s/helper/metadata * combined positional args to _request into options dict * metadataRequest reuses _request() * moved consumption of this.httpMethod into _request, can be overwritten in options due to spread-after (cherry picked from commit 006286ac05b7b74434bafaa0b4ecbd51e33bb0ac) --- .../datasource/prometheus/datasource.ts | 30 +++++++------------ .../prometheus/metric_find_query.ts | 8 ++--- 2 files changed, 15 insertions(+), 23 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 3eceaf6c622..3a2c78dce2d 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -5,6 +5,7 @@ import kbn from 'app/core/utils/kbn'; import * as dateMath from 'app/core/utils/datemath'; import PrometheusMetricFindQuery from './metric_find_query'; import { ResultTransformer } from './result_transformer'; +import { BackendSrv } from 'app/core/services/backend_srv'; export function prometheusRegularEscape(value) { return value.replace(/'/g, "\\\\'"); @@ -29,7 +30,7 @@ export class PrometheusDatasource { resultTransformer: ResultTransformer; /** @ngInject */ - constructor(instanceSettings, private $q, private backendSrv, private templateSrv, private timeSrv) { + constructor(instanceSettings, private $q, private backendSrv: BackendSrv, private templateSrv, private timeSrv) { this.type = 'prometheus'; this.editorSrc = 'app/features/prometheus/partials/query.editor.html'; this.name = instanceSettings.name; @@ -43,13 +44,13 @@ export class PrometheusDatasource { this.resultTransformer = new ResultTransformer(templateSrv); } - _request(method, url, data?, requestId?) { + _request(url, data?, options?: any) { var options: any = { url: this.url + url, - method: method, - requestId: requestId, + method: this.httpMethod, + ...options, }; - if (method === 'GET') { + if (options.method === 'GET') { if (!_.isEmpty(data)) { options.url = options.url + @@ -82,17 +83,8 @@ export class PrometheusDatasource { } // Use this for tab completion features, wont publish response to other components - helperRequest(url) { - const options: any = { - url: this.url + url, - silent: true, - }; - - if (this.basicAuth || this.withCredentials) { - options.withCredentials = true; - } - - return this.backendSrv.datasourceRequest(options); + metadataRequest(url) { + return this._request(url, null, { silent: true }); } interpolateQueryExpr(value, variable, defaultFormatFn) { @@ -220,7 +212,7 @@ export class PrometheusDatasource { end: end, step: query.step, }; - return this._request(this.httpMethod, url, data, query.requestId); + return this._request(url, data, { requestId: query.requestId }); } performInstantQuery(query, time) { @@ -229,7 +221,7 @@ export class PrometheusDatasource { query: query.expr, time: time, }; - return this._request(this.httpMethod, url, data, query.requestId); + return this._request(url, data, { requestId: query.requestId }); } performSuggestQuery(query, cache = false) { @@ -243,7 +235,7 @@ export class PrometheusDatasource { ); } - return this.helperRequest(url).then(result => { + return this.metadataRequest(url).then(result => { this.metricsNameCache = { data: result.data.data, expire: Date.now() + 60 * 1000, diff --git a/public/app/plugins/datasource/prometheus/metric_find_query.ts b/public/app/plugins/datasource/prometheus/metric_find_query.ts index c58f5c097b9..337cd74c14c 100644 --- a/public/app/plugins/datasource/prometheus/metric_find_query.ts +++ b/public/app/plugins/datasource/prometheus/metric_find_query.ts @@ -46,7 +46,7 @@ export default class PrometheusMetricFindQuery { // return label values globally url = '/api/v1/label/' + label + '/values'; - return this.datasource.helperRequest(url).then(function(result) { + return this.datasource.metadataRequest(url).then(function(result) { return _.map(result.data.data, function(value) { return { text: value }; }); @@ -56,7 +56,7 @@ export default class PrometheusMetricFindQuery { var end = this.datasource.getPrometheusTime(this.range.to, true); url = '/api/v1/series?match[]=' + encodeURIComponent(metric) + '&start=' + start + '&end=' + end; - return this.datasource.helperRequest(url).then(function(result) { + return this.datasource.metadataRequest(url).then(function(result) { var _labels = _.map(result.data.data, function(metric) { return metric[label] || ''; }).filter(function(label) { @@ -76,7 +76,7 @@ export default class PrometheusMetricFindQuery { metricNameQuery(metricFilterPattern) { var url = '/api/v1/label/__name__/values'; - return this.datasource.helperRequest(url).then(function(result) { + return this.datasource.metadataRequest(url).then(function(result) { return _.chain(result.data.data) .filter(function(metricName) { var r = new RegExp(metricFilterPattern); @@ -120,7 +120,7 @@ export default class PrometheusMetricFindQuery { var url = '/api/v1/series?match[]=' + encodeURIComponent(query) + '&start=' + start + '&end=' + end; var self = this; - return this.datasource.helperRequest(url).then(function(result) { + return this.datasource.metadataRequest(url).then(function(result) { return _.map(result.data.data, function(metric) { return { text: self.datasource.getOriginalMetricName(metric), From 9df8c4fe86a7fb3b10abba9b282913df6d269b90 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Tue, 24 Apr 2018 16:26:46 +0200 Subject: [PATCH 19/23] force GET for metadataRequests, w/ test (cherry picked from commit 707700ac7dd1d938f281c110113d274907417692) --- .../datasource/prometheus/datasource.ts | 2 +- .../prometheus/specs/datasource.jest.ts | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 3a2c78dce2d..cbf701a0abe 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -84,7 +84,7 @@ export class PrometheusDatasource { // Use this for tab completion features, wont publish response to other components metadataRequest(url) { - return this._request(url, null, { silent: true }); + return this._request(url, null, { method: 'GET', silent: true }); } interpolateQueryExpr(value, variable, defaultFormatFn) { diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts index d2620b93bbc..a997a2d8233 100644 --- a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts +++ b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts @@ -14,6 +14,7 @@ describe('PrometheusDatasource', () => { }; ctx.backendSrvMock = {}; + ctx.templateSrvMock = { replace: a => a, }; @@ -23,6 +24,25 @@ describe('PrometheusDatasource', () => { ctx.ds = new PrometheusDatasource(instanceSettings, q, ctx.backendSrvMock, ctx.templateSrvMock, ctx.timeSrvMock); }); + describe('Datasource metadata requests', () => { + it('should perform a GET request with the default config', () => { + ctx.backendSrvMock.datasourceRequest = jest.fn(); + ctx.ds.metadataRequest('/foo'); + expect(ctx.backendSrvMock.datasourceRequest.mock.calls.length).toBe(1); + expect(ctx.backendSrvMock.datasourceRequest.mock.calls[0][0].method).toBe('GET'); + }); + + it('should still perform a GET request with the DS HTTP method set to POST', () => { + ctx.backendSrvMock.datasourceRequest = jest.fn(); + const postSettings = _.cloneDeep(instanceSettings); + postSettings.jsonData.httpMethod = 'POST'; + const ds = new PrometheusDatasource(postSettings, q, ctx.backendSrvMock, ctx.templateSrvMock, ctx.timeSrvMock); + ds.metadataRequest('/foo'); + expect(ctx.backendSrvMock.datasourceRequest.mock.calls.length).toBe(1); + expect(ctx.backendSrvMock.datasourceRequest.mock.calls[0][0].method).toBe('GET'); + }); + }); + describe('When converting prometheus histogram to heatmap format', () => { beforeEach(() => { ctx.query = { From 70f4797a033005f272ac00e31176b7c32f8554e1 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 25 Apr 2018 15:36:00 +0200 Subject: [PATCH 20/23] prometheus: fix variable query to fallback correctly to series query Using a query of for example up or up{job=job1} (cherry picked from commit 6687409efba0efb2e6b71625b9782370b19ff111) --- public/app/plugins/datasource/prometheus/datasource.ts | 4 ++++ public/app/plugins/datasource/prometheus/metric_find_query.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index cbf701a0abe..2a8b3069a53 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -329,4 +329,8 @@ export class PrometheusDatasource { } return Math.ceil(date.valueOf() / 1000); } + + getOriginalMetricName(labelData) { + return this.resultTransformer.getOriginalMetricName(labelData); + } } diff --git a/public/app/plugins/datasource/prometheus/metric_find_query.ts b/public/app/plugins/datasource/prometheus/metric_find_query.ts index 337cd74c14c..13b6d7df8e3 100644 --- a/public/app/plugins/datasource/prometheus/metric_find_query.ts +++ b/public/app/plugins/datasource/prometheus/metric_find_query.ts @@ -121,7 +121,7 @@ export default class PrometheusMetricFindQuery { var self = this; return this.datasource.metadataRequest(url).then(function(result) { - return _.map(result.data.data, function(metric) { + return _.map(result.data.data, metric => { return { text: self.datasource.getOriginalMetricName(metric), expandable: true, From 08963b1414a96a1ea74d8abaa94c90473ebaed4f Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 25 Apr 2018 15:36:47 +0200 Subject: [PATCH 21/23] prometheus: convert metric find query tests to jest (cherry picked from commit f112e38266a4cbb96c170fc213e2533d0c06c814) --- .../prometheus/specs/datasource.jest.ts | 20 ++ .../specs/metric_find_query.jest.ts | 205 ++++++++++++++++++ .../specs/metric_find_query_specs.ts | 181 ---------------- 3 files changed, 225 insertions(+), 181 deletions(-) create mode 100644 public/app/plugins/datasource/prometheus/specs/metric_find_query.jest.ts delete mode 100644 public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts index a997a2d8233..2ab2895d731 100644 --- a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts +++ b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts @@ -43,6 +43,26 @@ describe('PrometheusDatasource', () => { }); }); + describe('When performing performSuggestQuery', () => { + it('should cache response', async () => { + ctx.backendSrvMock.datasourceRequest.mockReturnValue( + Promise.resolve({ + status: 'success', + data: { data: ['value1', 'value2', 'value3'] }, + }) + ); + + let results = await ctx.ds.performSuggestQuery('value', true); + + expect(results).toHaveLength(3); + + ctx.backendSrvMock.datasourceRequest.mockReset(); + results = await ctx.ds.performSuggestQuery('value', true); + + expect(results).toHaveLength(3); + }); + }); + describe('When converting prometheus histogram to heatmap format', () => { beforeEach(() => { ctx.query = { diff --git a/public/app/plugins/datasource/prometheus/specs/metric_find_query.jest.ts b/public/app/plugins/datasource/prometheus/specs/metric_find_query.jest.ts new file mode 100644 index 00000000000..88f6830cd31 --- /dev/null +++ b/public/app/plugins/datasource/prometheus/specs/metric_find_query.jest.ts @@ -0,0 +1,205 @@ +import moment from 'moment'; +import { PrometheusDatasource } from '../datasource'; +import PrometheusMetricFindQuery from '../metric_find_query'; +import q from 'q'; + +describe('PrometheusMetricFindQuery', function() { + let instanceSettings = { + url: 'proxied', + directUrl: 'direct', + user: 'test', + password: 'mupp', + jsonData: { httpMethod: 'GET' }, + }; + const raw = { + from: moment.utc('2018-04-25 10:00'), + to: moment.utc('2018-04-25 11:00'), + }; + let ctx: any = { + backendSrvMock: { + datasourceRequest: jest.fn(() => Promise.resolve({})), + }, + templateSrvMock: { + replace: a => a, + }, + timeSrvMock: { + timeRange: () => ({ + from: raw.from, + to: raw.to, + raw: raw, + }), + }, + }; + + ctx.setupMetricFindQuery = (data: any) => { + ctx.backendSrvMock.datasourceRequest.mockReturnValue(Promise.resolve({ status: 'success', data: data.response })); + return new PrometheusMetricFindQuery(ctx.ds, data.query, ctx.timeSrvMock); + }; + + beforeEach(() => { + ctx.backendSrvMock.datasourceRequest.mockReset(); + ctx.ds = new PrometheusDatasource(instanceSettings, q, ctx.backendSrvMock, ctx.templateSrvMock, ctx.timeSrvMock); + }); + + describe('When performing metricFindQuery', () => { + it('label_values(resource) should generate label search query', async () => { + const query = ctx.setupMetricFindQuery({ + query: 'label_values(resource)', + response: { + data: ['value1', 'value2', 'value3'], + }, + }); + const results = await query.process(); + + expect(results).toHaveLength(3); + expect(ctx.backendSrvMock.datasourceRequest).toHaveBeenCalledTimes(1); + expect(ctx.backendSrvMock.datasourceRequest).toHaveBeenCalledWith({ + method: 'GET', + url: 'proxied/api/v1/label/resource/values', + silent: true, + }); + }); + + it('label_values(metric, resource) should generate series query with correct time', async () => { + const query = ctx.setupMetricFindQuery({ + query: 'label_values(metric, resource)', + response: { + data: [ + { __name__: 'metric', resource: 'value1' }, + { __name__: 'metric', resource: 'value2' }, + { __name__: 'metric', resource: 'value3' }, + ], + }, + }); + const results = await query.process(); + + expect(results).toHaveLength(3); + expect(ctx.backendSrvMock.datasourceRequest).toHaveBeenCalledTimes(1); + expect(ctx.backendSrvMock.datasourceRequest).toHaveBeenCalledWith({ + method: 'GET', + url: `proxied/api/v1/series?match[]=metric&start=${raw.from.unix()}&end=${raw.to.unix()}`, + silent: true, + }); + }); + + it('label_values(metric{label1="foo", label2="bar", label3="baz"}, resource) should generate series query with correct time', async () => { + const query = ctx.setupMetricFindQuery({ + query: 'label_values(metric{label1="foo", label2="bar", label3="baz"}, resource)', + response: { + data: [ + { __name__: 'metric', resource: 'value1' }, + { __name__: 'metric', resource: 'value2' }, + { __name__: 'metric', resource: 'value3' }, + ], + }, + }); + const results = await query.process(); + + expect(results).toHaveLength(3); + expect(ctx.backendSrvMock.datasourceRequest).toHaveBeenCalledTimes(1); + expect(ctx.backendSrvMock.datasourceRequest).toHaveBeenCalledWith({ + method: 'GET', + url: `proxied/api/v1/series?match[]=${encodeURIComponent( + 'metric{label1="foo", label2="bar", label3="baz"}' + )}&start=${raw.from.unix()}&end=${raw.to.unix()}`, + silent: true, + }); + }); + + it('label_values(metric, resource) result should not contain empty string', async () => { + const query = ctx.setupMetricFindQuery({ + query: 'label_values(metric, resource)', + response: { + data: [ + { __name__: 'metric', resource: 'value1' }, + { __name__: 'metric', resource: 'value2' }, + { __name__: 'metric', resource: '' }, + ], + }, + }); + const results = await query.process(); + + expect(results).toHaveLength(2); + expect(results[0].text).toBe('value1'); + expect(results[1].text).toBe('value2'); + expect(ctx.backendSrvMock.datasourceRequest).toHaveBeenCalledTimes(1); + expect(ctx.backendSrvMock.datasourceRequest).toHaveBeenCalledWith({ + method: 'GET', + url: `proxied/api/v1/series?match[]=metric&start=${raw.from.unix()}&end=${raw.to.unix()}`, + silent: true, + }); + }); + + it('metrics(metric.*) should generate metric name query', async () => { + const query = ctx.setupMetricFindQuery({ + query: 'metrics(metric.*)', + response: { + data: ['metric1', 'metric2', 'metric3', 'nomatch'], + }, + }); + const results = await query.process(); + + expect(results).toHaveLength(3); + expect(ctx.backendSrvMock.datasourceRequest).toHaveBeenCalledTimes(1); + expect(ctx.backendSrvMock.datasourceRequest).toHaveBeenCalledWith({ + method: 'GET', + url: 'proxied/api/v1/label/__name__/values', + silent: true, + }); + }); + + it('query_result(metric) should generate metric name query', async () => { + const query = ctx.setupMetricFindQuery({ + query: 'query_result(metric)', + response: { + data: { + resultType: 'vector', + result: [ + { + metric: { __name__: 'metric', job: 'testjob' }, + value: [1443454528.0, '3846'], + }, + ], + }, + }, + }); + const results = await query.process(); + + expect(results).toHaveLength(1); + expect(results[0].text).toBe('metric{job="testjob"} 3846 1443454528000'); + expect(ctx.backendSrvMock.datasourceRequest).toHaveBeenCalledTimes(1); + expect(ctx.backendSrvMock.datasourceRequest).toHaveBeenCalledWith({ + method: 'GET', + url: `proxied/api/v1/query?query=metric&time=${raw.to.unix()}`, + requestId: undefined, + }); + }); + + it('up{job="job1"} should fallback using generate series query', async () => { + const query = ctx.setupMetricFindQuery({ + query: 'up{job="job1"}', + response: { + data: [ + { __name__: 'up', instance: '127.0.0.1:1234', job: 'job1' }, + { __name__: 'up', instance: '127.0.0.1:5678', job: 'job1' }, + { __name__: 'up', instance: '127.0.0.1:9102', job: 'job1' }, + ], + }, + }); + const results = await query.process(); + + expect(results).toHaveLength(3); + expect(results[0].text).toBe('up{instance="127.0.0.1:1234",job="job1"}'); + expect(results[1].text).toBe('up{instance="127.0.0.1:5678",job="job1"}'); + expect(results[2].text).toBe('up{instance="127.0.0.1:9102",job="job1"}'); + expect(ctx.backendSrvMock.datasourceRequest).toHaveBeenCalledTimes(1); + expect(ctx.backendSrvMock.datasourceRequest).toHaveBeenCalledWith({ + method: 'GET', + url: `proxied/api/v1/series?match[]=${encodeURIComponent( + 'up{job="job1"}' + )}&start=${raw.from.unix()}&end=${raw.to.unix()}`, + silent: true, + }); + }); + }); +}); 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 deleted file mode 100644 index e5d7aa81210..00000000000 --- a/public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts +++ /dev/null @@ -1,181 +0,0 @@ -import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common'; - -import moment from 'moment'; -import helpers from 'test/specs/helpers'; -import { PrometheusDatasource } from '../datasource'; -import PrometheusMetricFindQuery from '../metric_find_query'; - -describe('PrometheusMetricFindQuery', function() { - var ctx = new helpers.ServiceTestContext(); - var instanceSettings = { - url: 'proxied', - directUrl: 'direct', - user: 'test', - password: 'mupp', - jsonData: { httpMethod: 'GET' }, - }; - - beforeEach(angularMocks.module('grafana.core')); - beforeEach(angularMocks.module('grafana.services')); - beforeEach( - angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) { - ctx.$q = $q; - ctx.$httpBackend = $httpBackend; - ctx.$rootScope = $rootScope; - ctx.ds = $injector.instantiate(PrometheusDatasource, { - instanceSettings: instanceSettings, - }); - $httpBackend.when('GET', /\.html$/).respond(''); - }) - ); - - describe('When performing metricFindQuery', function() { - var results; - var response; - it('label_values(resource) should generate label search query', function() { - response = { - status: 'success', - data: ['value1', 'value2', 'value3'], - }; - ctx.$httpBackend.expect('GET', 'proxied/api/v1/label/resource/values').respond(response); - var pm = new PrometheusMetricFindQuery(ctx.ds, 'label_values(resource)', ctx.timeSrv); - pm.process().then(function(data) { - results = data; - }); - ctx.$httpBackend.flush(); - ctx.$rootScope.$apply(); - expect(results.length).to.be(3); - }); - it('label_values(metric, 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&start=.*&end=.*/).respond(response); - var pm = new PrometheusMetricFindQuery(ctx.ds, 'label_values(metric, resource)', ctx.timeSrv); - pm.process().then(function(data) { - results = data; - }); - ctx.$httpBackend.flush(); - ctx.$rootScope.$apply(); - expect(results.length).to.be(3); - }); - it('label_values(metric, resource) should pass correct time', function() { - ctx.timeSrv.setTime({ - from: moment.utc('2011-01-01'), - to: moment.utc('2015-01-01'), - }); - ctx.$httpBackend - .expect('GET', /proxied\/api\/v1\/series\?match\[\]=metric&start=1293840000&end=1420070400/) - .respond(response); - var pm = new PrometheusMetricFindQuery(ctx.ds, 'label_values(metric, resource)', ctx.timeSrv); - pm.process().then(function(data) { - results = data; - }); - ctx.$httpBackend.flush(); - ctx.$rootScope.$apply(); - }); - 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&start=.*&end=.*/).respond(response); - var pm = new PrometheusMetricFindQuery(ctx.ds, 'label_values(metric, resource)', ctx.timeSrv); - pm.process().then(function(data) { - results = data; - }); - ctx.$httpBackend.flush(); - ctx.$rootScope.$apply(); - expect(results.length).to.be(3); - }); - it('label_values(metric, resource) result should not contain empty string', function() { - response = { - status: 'success', - data: [ - { __name__: 'metric', resource: 'value1' }, - { __name__: 'metric', resource: 'value2' }, - { __name__: 'metric', resource: '' }, - ], - }; - ctx.$httpBackend.expect('GET', /proxied\/api\/v1\/series\?match\[\]=metric&start=.*&end=.*/).respond(response); - var pm = new PrometheusMetricFindQuery(ctx.ds, 'label_values(metric, resource)', ctx.timeSrv); - pm.process().then(function(data) { - results = data; - }); - ctx.$httpBackend.flush(); - ctx.$rootScope.$apply(); - expect(results.length).to.be(2); - expect(results[0].text).to.be('value1'); - expect(results[1].text).to.be('value2'); - }); - it('metrics(metric.*) should generate metric name query', function() { - response = { - status: 'success', - data: ['metric1', 'metric2', 'metric3', 'nomatch'], - }; - ctx.$httpBackend.expect('GET', 'proxied/api/v1/label/__name__/values').respond(response); - var pm = new PrometheusMetricFindQuery(ctx.ds, 'metrics(metric.*)', ctx.timeSrv); - pm.process().then(function(data) { - results = data; - }); - ctx.$httpBackend.flush(); - ctx.$rootScope.$apply(); - expect(results.length).to.be(3); - }); - it('query_result(metric) should generate metric name query', function() { - response = { - status: 'success', - data: { - resultType: 'vector', - result: [ - { - metric: { __name__: 'metric', job: 'testjob' }, - value: [1443454528.0, '3846'], - }, - ], - }, - }; - ctx.$httpBackend.expect('GET', /proxied\/api\/v1\/query\?query=metric&time=.*/).respond(response); - var pm = new PrometheusMetricFindQuery(ctx.ds, 'query_result(metric)', ctx.timeSrv); - pm.process().then(function(data) { - results = data; - }); - ctx.$httpBackend.flush(); - ctx.$rootScope.$apply(); - expect(results.length).to.be(1); - expect(results[0].text).to.be('metric{job="testjob"} 3846 1443454528000'); - }); - }); - - describe('When performing performSuggestQuery', function() { - var results; - var response; - it('cache response', function() { - response = { - status: 'success', - data: ['value1', 'value2', 'value3'], - }; - ctx.$httpBackend.expect('GET', 'proxied/api/v1/label/__name__/values').respond(response); - ctx.ds.performSuggestQuery('value', true).then(function(data) { - results = data; - }); - ctx.$httpBackend.flush(); - ctx.$rootScope.$apply(); - expect(results.length).to.be(3); - ctx.ds.performSuggestQuery('value', true).then(function(data) { - // get from cache, no need to flush - results = data; - expect(results.length).to.be(3); - }); - }); - }); -}); From 61de54be5aa899ff60bc61ec089efe4f3e53eba2 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 26 Apr 2018 15:49:22 +0200 Subject: [PATCH 22/23] fix so that google analytics script are cached (cherry picked from commit ddeba41638806bf7174e7a51ce78ecc53dd29309) --- public/app/core/services/analytics.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/public/app/core/services/analytics.ts b/public/app/core/services/analytics.ts index 370773154e5..4f9994302ab 100644 --- a/public/app/core/services/analytics.ts +++ b/public/app/core/services/analytics.ts @@ -7,7 +7,11 @@ export class Analytics { constructor(private $rootScope, private $location) {} gaInit() { - $.getScript('https://www.google-analytics.com/analytics.js'); // jQuery shortcut + $.ajax({ + url: 'https://www.google-analytics.com/analytics.js', + dataType: 'script', + cache: true, + }); var ga = ((window).ga = (window).ga || function() { From ebffcc21cf928c0c0247be41f871c93b2a611f5c Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 26 Apr 2018 16:34:28 +0200 Subject: [PATCH 23/23] 5.1.0 release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b378945dd15..5d0c1be47a4 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "company": "Grafana Labs" }, "name": "grafana", - "version": "5.1.0-beta1", + "version": "5.1.0", "repository": { "type": "git", "url": "http://github.com/grafana/grafana.git"