mirror of
https://github.com/grafana/grafana.git
synced 2025-12-23 21:34:21 +08:00
Compare commits
17 Commits
sriram/pos
...
v5.2.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30c882c18d | ||
|
|
e5836064ce | ||
|
|
f7cb827944 | ||
|
|
f76cafa68e | ||
|
|
cdae9126ed | ||
|
|
b4c1df11f6 | ||
|
|
7c94d5cd1a | ||
|
|
74d6b5fc1c | ||
|
|
af42e0836a | ||
|
|
f453fbe8ef | ||
|
|
8d635efda0 | ||
|
|
0f2e879339 | ||
|
|
e51dd88260 | ||
|
|
984293cc52 | ||
|
|
9a1a9584b7 | ||
|
|
8a69ffb007 | ||
|
|
faa5e699d2 |
@@ -4,7 +4,7 @@
|
|||||||
"company": "Grafana Labs"
|
"company": "Grafana Labs"
|
||||||
},
|
},
|
||||||
"name": "grafana",
|
"name": "grafana",
|
||||||
"version": "5.2.0-pre1",
|
"version": "5.2.0-beta3",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "http://github.com/grafana/grafana.git"
|
"url": "http://github.com/grafana/grafana.git"
|
||||||
|
|||||||
@@ -308,6 +308,10 @@ func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) {
|
|||||||
} else {
|
} else {
|
||||||
filter_replace = getLdapAttr(a.server.GroupSearchFilterUserAttribute, searchResult)
|
filter_replace = getLdapAttr(a.server.GroupSearchFilterUserAttribute, searchResult)
|
||||||
}
|
}
|
||||||
|
if a.server.GroupSearchFilterUserAttribute == "dn" {
|
||||||
|
filter_replace = searchResult.Entries[0].DN
|
||||||
|
}
|
||||||
|
|
||||||
filter := strings.Replace(a.server.GroupSearchFilter, "%s", ldap.EscapeFilter(filter_replace), -1)
|
filter := strings.Replace(a.server.GroupSearchFilter, "%s", ldap.EscapeFilter(filter_replace), -1)
|
||||||
|
|
||||||
a.log.Info("Searching for user's groups", "filter", filter)
|
a.log.Info("Searching for user's groups", "filter", filter)
|
||||||
@@ -330,7 +334,11 @@ func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) {
|
|||||||
|
|
||||||
if len(groupSearchResult.Entries) > 0 {
|
if len(groupSearchResult.Entries) > 0 {
|
||||||
for i := range groupSearchResult.Entries {
|
for i := range groupSearchResult.Entries {
|
||||||
memberOf = append(memberOf, getLdapAttrN(a.server.Attr.MemberOf, groupSearchResult, i))
|
if a.server.Attr.MemberOf == "dn" {
|
||||||
|
memberOf = append(memberOf, groupSearchResult.Entries[i].DN)
|
||||||
|
} else {
|
||||||
|
memberOf = append(memberOf, getLdapAttrN(a.server.Attr.MemberOf, groupSearchResult, i))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ func (g *dashboardGuardianImpl) checkAcl(permission m.PermissionType, acl []*m.D
|
|||||||
|
|
||||||
for _, p := range acl {
|
for _, p := range acl {
|
||||||
// user match
|
// user match
|
||||||
if !g.user.IsAnonymous {
|
if !g.user.IsAnonymous && p.UserId > 0 {
|
||||||
if p.UserId == g.user.UserId && p.Permission >= permission {
|
if p.UserId == g.user.UserId && p.Permission >= permission {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,6 +162,11 @@ func TestGuardianViewer(t *testing.T) {
|
|||||||
sc.parentFolderPermissionScenario(VIEWER, m.PERMISSION_EDIT, EDITOR_ACCESS)
|
sc.parentFolderPermissionScenario(VIEWER, m.PERMISSION_EDIT, EDITOR_ACCESS)
|
||||||
sc.parentFolderPermissionScenario(VIEWER, m.PERMISSION_VIEW, VIEWER_ACCESS)
|
sc.parentFolderPermissionScenario(VIEWER, m.PERMISSION_VIEW, VIEWER_ACCESS)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
apiKeyScenario("Given api key with viewer role", t, m.ROLE_VIEWER, func(sc *scenarioContext) {
|
||||||
|
// dashboard has default permissions
|
||||||
|
sc.defaultPermissionScenario(VIEWER, m.PERMISSION_EDIT, VIEWER_ACCESS)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,7 +272,7 @@ func (sc *scenarioContext) verifyExpectedPermissionsFlags() {
|
|||||||
actualFlag = NO_ACCESS
|
actualFlag = NO_ACCESS
|
||||||
}
|
}
|
||||||
|
|
||||||
if sc.expectedFlags&actualFlag != sc.expectedFlags {
|
if actualFlag&sc.expectedFlags != actualFlag {
|
||||||
sc.reportFailure(tc, sc.expectedFlags.String(), actualFlag.String())
|
sc.reportFailure(tc, sc.expectedFlags.String(), actualFlag.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,27 @@ func orgRoleScenario(desc string, t *testing.T, role m.RoleType, fn scenarioFunc
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func apiKeyScenario(desc string, t *testing.T, role m.RoleType, fn scenarioFunc) {
|
||||||
|
user := &m.SignedInUser{
|
||||||
|
UserId: 0,
|
||||||
|
OrgId: orgID,
|
||||||
|
OrgRole: role,
|
||||||
|
ApiKeyId: 10,
|
||||||
|
}
|
||||||
|
guard := New(dashboardID, orgID, user)
|
||||||
|
sc := &scenarioContext{
|
||||||
|
t: t,
|
||||||
|
orgRoleScenario: desc,
|
||||||
|
givenUser: user,
|
||||||
|
givenDashboardID: dashboardID,
|
||||||
|
g: guard,
|
||||||
|
}
|
||||||
|
|
||||||
|
Convey(desc, func() {
|
||||||
|
fn(sc)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func permissionScenario(desc string, dashboardID int64, sc *scenarioContext, permissions []*m.DashboardAclInfoDTO, fn scenarioFunc) {
|
func permissionScenario(desc string, dashboardID int64, sc *scenarioContext, permissions []*m.DashboardAclInfoDTO, fn scenarioFunc) {
|
||||||
bus.ClearBusHandlers()
|
bus.ClearBusHandlers()
|
||||||
|
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ func TestAccountDataAccess(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
Convey("Can set using org", func() {
|
Convey("Can set using org", func() {
|
||||||
cmd := m.SetUsingOrgCommand{UserId: ac2.Id, OrgId: ac1.Id}
|
cmd := m.SetUsingOrgCommand{UserId: ac2.Id, OrgId: ac1.OrgId}
|
||||||
err := SetUsingOrg(&cmd)
|
err := SetUsingOrg(&cmd)
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
@@ -159,13 +159,25 @@ func TestAccountDataAccess(t *testing.T) {
|
|||||||
err := GetSignedInUser(&query)
|
err := GetSignedInUser(&query)
|
||||||
|
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
So(query.Result.OrgId, ShouldEqual, ac1.Id)
|
So(query.Result.OrgId, ShouldEqual, ac1.OrgId)
|
||||||
So(query.Result.Email, ShouldEqual, "ac2@test.com")
|
So(query.Result.Email, ShouldEqual, "ac2@test.com")
|
||||||
So(query.Result.Name, ShouldEqual, "ac2 name")
|
So(query.Result.Name, ShouldEqual, "ac2 name")
|
||||||
So(query.Result.Login, ShouldEqual, "ac2")
|
So(query.Result.Login, ShouldEqual, "ac2")
|
||||||
So(query.Result.OrgName, ShouldEqual, "ac1@test.com")
|
So(query.Result.OrgName, ShouldEqual, "ac1@test.com")
|
||||||
So(query.Result.OrgRole, ShouldEqual, "Viewer")
|
So(query.Result.OrgRole, ShouldEqual, "Viewer")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Convey("Should set last org as current when removing user from current", func() {
|
||||||
|
remCmd := m.RemoveOrgUserCommand{OrgId: ac1.OrgId, UserId: ac2.Id}
|
||||||
|
err := RemoveOrgUser(&remCmd)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
query := m.GetSignedInUserQuery{UserId: ac2.Id}
|
||||||
|
err = GetSignedInUser(&query)
|
||||||
|
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(query.Result.OrgId, ShouldEqual, ac2.OrgId)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
Convey("Cannot delete last admin org user", func() {
|
Convey("Cannot delete last admin org user", func() {
|
||||||
|
|||||||
@@ -20,7 +20,14 @@ func init() {
|
|||||||
func AddOrgUser(cmd *m.AddOrgUserCommand) error {
|
func AddOrgUser(cmd *m.AddOrgUserCommand) error {
|
||||||
return inTransaction(func(sess *DBSession) error {
|
return inTransaction(func(sess *DBSession) error {
|
||||||
// check if user exists
|
// check if user exists
|
||||||
if res, err := sess.Query("SELECT 1 from org_user WHERE org_id=? and user_id=?", cmd.OrgId, cmd.UserId); err != nil {
|
var user m.User
|
||||||
|
if exists, err := sess.Id(cmd.UserId).Get(&user); err != nil {
|
||||||
|
return err
|
||||||
|
} else if !exists {
|
||||||
|
return m.ErrUserNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
if res, err := sess.Query("SELECT 1 from org_user WHERE org_id=? and user_id=?", cmd.OrgId, user.Id); err != nil {
|
||||||
return err
|
return err
|
||||||
} else if len(res) == 1 {
|
} else if len(res) == 1 {
|
||||||
return m.ErrOrgUserAlreadyAdded
|
return m.ErrOrgUserAlreadyAdded
|
||||||
@@ -41,7 +48,26 @@ func AddOrgUser(cmd *m.AddOrgUserCommand) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_, err := sess.Insert(&entity)
|
_, err := sess.Insert(&entity)
|
||||||
return err
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var userOrgs []*m.UserOrgDTO
|
||||||
|
sess.Table("org_user")
|
||||||
|
sess.Join("INNER", "org", "org_user.org_id=org.id")
|
||||||
|
sess.Where("org_user.user_id=? AND org_user.org_id=?", user.Id, user.OrgId)
|
||||||
|
sess.Cols("org.name", "org_user.role", "org_user.org_id")
|
||||||
|
err = sess.Find(&userOrgs)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(userOrgs) == 0 {
|
||||||
|
return setUsingOrgInTransaction(sess, user.Id, cmd.OrgId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,6 +136,14 @@ func GetOrgUsers(query *m.GetOrgUsersQuery) error {
|
|||||||
|
|
||||||
func RemoveOrgUser(cmd *m.RemoveOrgUserCommand) error {
|
func RemoveOrgUser(cmd *m.RemoveOrgUserCommand) error {
|
||||||
return inTransaction(func(sess *DBSession) error {
|
return inTransaction(func(sess *DBSession) error {
|
||||||
|
// check if user exists
|
||||||
|
var user m.User
|
||||||
|
if exists, err := sess.Id(cmd.UserId).Get(&user); err != nil {
|
||||||
|
return err
|
||||||
|
} else if !exists {
|
||||||
|
return m.ErrUserNotFound
|
||||||
|
}
|
||||||
|
|
||||||
deletes := []string{
|
deletes := []string{
|
||||||
"DELETE FROM org_user WHERE org_id=? and user_id=?",
|
"DELETE FROM org_user WHERE org_id=? and user_id=?",
|
||||||
"DELETE FROM dashboard_acl WHERE org_id=? and user_id = ?",
|
"DELETE FROM dashboard_acl WHERE org_id=? and user_id = ?",
|
||||||
@@ -123,6 +157,32 @@ func RemoveOrgUser(cmd *m.RemoveOrgUserCommand) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var userOrgs []*m.UserOrgDTO
|
||||||
|
sess.Table("org_user")
|
||||||
|
sess.Join("INNER", "org", "org_user.org_id=org.id")
|
||||||
|
sess.Where("org_user.user_id=?", user.Id)
|
||||||
|
sess.Cols("org.name", "org_user.role", "org_user.org_id")
|
||||||
|
err := sess.Find(&userOrgs)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hasCurrentOrgSet := false
|
||||||
|
for _, userOrg := range userOrgs {
|
||||||
|
if user.OrgId == userOrg.OrgId {
|
||||||
|
hasCurrentOrgSet = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasCurrentOrgSet && len(userOrgs) > 0 {
|
||||||
|
err = setUsingOrgInTransaction(sess, user.Id, userOrgs[0].OrgId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return validateOneAdminLeftInOrg(cmd.OrgId, sess)
|
return validateOneAdminLeftInOrg(cmd.OrgId, sess)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -290,16 +290,20 @@ func SetUsingOrg(cmd *m.SetUsingOrgCommand) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return inTransaction(func(sess *DBSession) error {
|
return inTransaction(func(sess *DBSession) error {
|
||||||
user := m.User{
|
return setUsingOrgInTransaction(sess, cmd.UserId, cmd.OrgId)
|
||||||
Id: cmd.UserId,
|
|
||||||
OrgId: cmd.OrgId,
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := sess.Id(cmd.UserId).Update(&user)
|
|
||||||
return err
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setUsingOrgInTransaction(sess *DBSession, userID int64, orgID int64) error {
|
||||||
|
user := m.User{
|
||||||
|
Id: userID,
|
||||||
|
OrgId: orgID,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := sess.Id(userID).Update(&user)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func GetUserProfile(query *m.GetUserProfileQuery) error {
|
func GetUserProfile(query *m.GetUserProfileQuery) error {
|
||||||
var user m.User
|
var user m.User
|
||||||
has, err := x.Id(query.UserId).Get(&user)
|
has, err := x.Id(query.UserId).Get(&user)
|
||||||
|
|||||||
@@ -96,33 +96,33 @@ func TestUserDataAccess(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
Convey("when a user is an org member and has been assigned permissions", func() {
|
Convey("when a user is an org member and has been assigned permissions", func() {
|
||||||
err = AddOrgUser(&m.AddOrgUserCommand{LoginOrEmail: users[0].Login, Role: m.ROLE_VIEWER, OrgId: users[0].OrgId})
|
err = AddOrgUser(&m.AddOrgUserCommand{LoginOrEmail: users[1].Login, Role: m.ROLE_VIEWER, OrgId: users[0].OrgId, UserId: users[1].Id})
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
testHelperUpdateDashboardAcl(1, m.DashboardAcl{DashboardId: 1, OrgId: users[0].OrgId, UserId: users[0].Id, Permission: m.PERMISSION_EDIT})
|
testHelperUpdateDashboardAcl(1, m.DashboardAcl{DashboardId: 1, OrgId: users[0].OrgId, UserId: users[1].Id, Permission: m.PERMISSION_EDIT})
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
err = SavePreferences(&m.SavePreferencesCommand{UserId: users[0].Id, OrgId: users[0].OrgId, HomeDashboardId: 1, Theme: "dark"})
|
err = SavePreferences(&m.SavePreferencesCommand{UserId: users[1].Id, OrgId: users[0].OrgId, HomeDashboardId: 1, Theme: "dark"})
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
Convey("when the user is deleted", func() {
|
Convey("when the user is deleted", func() {
|
||||||
err = DeleteUser(&m.DeleteUserCommand{UserId: users[0].Id})
|
err = DeleteUser(&m.DeleteUserCommand{UserId: users[1].Id})
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
Convey("Should delete connected org users and permissions", func() {
|
Convey("Should delete connected org users and permissions", func() {
|
||||||
query := &m.GetOrgUsersQuery{OrgId: 1}
|
query := &m.GetOrgUsersQuery{OrgId: users[0].OrgId}
|
||||||
err = GetOrgUsersForTest(query)
|
err = GetOrgUsersForTest(query)
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
So(len(query.Result), ShouldEqual, 1)
|
So(len(query.Result), ShouldEqual, 1)
|
||||||
|
|
||||||
permQuery := &m.GetDashboardAclInfoListQuery{DashboardId: 1, OrgId: 1}
|
permQuery := &m.GetDashboardAclInfoListQuery{DashboardId: 1, OrgId: users[0].OrgId}
|
||||||
err = GetDashboardAclInfoList(permQuery)
|
err = GetDashboardAclInfoList(permQuery)
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
So(len(permQuery.Result), ShouldEqual, 0)
|
So(len(permQuery.Result), ShouldEqual, 0)
|
||||||
|
|
||||||
prefsQuery := &m.GetPreferencesQuery{OrgId: users[0].OrgId, UserId: users[0].Id}
|
prefsQuery := &m.GetPreferencesQuery{OrgId: users[0].OrgId, UserId: users[1].Id}
|
||||||
err = GetPreferences(prefsQuery)
|
err = GetPreferences(prefsQuery)
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
|||||||
25
public/app/core/specs/ticks.jest.ts
Normal file
25
public/app/core/specs/ticks.jest.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import * as ticks from '../utils/ticks';
|
||||||
|
|
||||||
|
describe('ticks', () => {
|
||||||
|
describe('getFlotTickDecimals()', () => {
|
||||||
|
let ctx: any = {};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
ctx.axis = {};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate decimals precision based on graph height', () => {
|
||||||
|
let dec = ticks.getFlotTickDecimals(0, 10, ctx.axis, 200);
|
||||||
|
expect(dec.tickDecimals).toBe(1);
|
||||||
|
expect(dec.scaledDecimals).toBe(1);
|
||||||
|
|
||||||
|
dec = ticks.getFlotTickDecimals(0, 100, ctx.axis, 200);
|
||||||
|
expect(dec.tickDecimals).toBe(0);
|
||||||
|
expect(dec.scaledDecimals).toBe(-1);
|
||||||
|
|
||||||
|
dec = ticks.getFlotTickDecimals(0, 1, ctx.axis, 200);
|
||||||
|
expect(dec.tickDecimals).toBe(2);
|
||||||
|
expect(dec.scaledDecimals).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import TimeSeries from 'app/core/time_series2';
|
import TimeSeries from 'app/core/time_series2';
|
||||||
|
import { updateLegendValues } from 'app/core/time_series2';
|
||||||
|
|
||||||
describe('TimeSeries', function() {
|
describe('TimeSeries', function() {
|
||||||
var points, series;
|
var points, series;
|
||||||
@@ -311,4 +312,55 @@ describe('TimeSeries', function() {
|
|||||||
expect(series.formatValue(-Infinity)).toBe('');
|
expect(series.formatValue(-Infinity)).toBe('');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('legend decimals', function() {
|
||||||
|
let series, panel;
|
||||||
|
let height = 200;
|
||||||
|
beforeEach(function() {
|
||||||
|
testData = {
|
||||||
|
alias: 'test',
|
||||||
|
datapoints: [[1, 2], [0, 3], [10, 4], [8, 5]],
|
||||||
|
};
|
||||||
|
series = new TimeSeries(testData);
|
||||||
|
series.getFlotPairs();
|
||||||
|
panel = {
|
||||||
|
decimals: null,
|
||||||
|
yaxes: [
|
||||||
|
{
|
||||||
|
decimals: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set decimals based on Y axis (expect calculated decimals = 1)', function() {
|
||||||
|
let data = [series];
|
||||||
|
// Expect ticks with this data will have decimals = 1
|
||||||
|
updateLegendValues(data, panel, height);
|
||||||
|
expect(data[0].decimals).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set decimals based on Y axis to 0 if calculated decimals = 0)', function() {
|
||||||
|
testData.datapoints = [[10, 2], [0, 3], [100, 4], [80, 5]];
|
||||||
|
series = new TimeSeries(testData);
|
||||||
|
series.getFlotPairs();
|
||||||
|
let data = [series];
|
||||||
|
updateLegendValues(data, panel, height);
|
||||||
|
expect(data[0].decimals).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set decimals to Y axis decimals + 1', function() {
|
||||||
|
panel.yaxes[0].decimals = 2;
|
||||||
|
let data = [series];
|
||||||
|
updateLegendValues(data, panel, height);
|
||||||
|
expect(data[0].decimals).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set decimals to legend decimals value if it was set explicitly', function() {
|
||||||
|
panel.decimals = 3;
|
||||||
|
let data = [series];
|
||||||
|
updateLegendValues(data, panel, height);
|
||||||
|
expect(data[0].decimals).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,23 +23,27 @@ function translateFillOption(fill) {
|
|||||||
* Calculate decimals for legend and update values for each series.
|
* Calculate decimals for legend and update values for each series.
|
||||||
* @param data series data
|
* @param data series data
|
||||||
* @param panel
|
* @param panel
|
||||||
|
* @param height
|
||||||
*/
|
*/
|
||||||
export function updateLegendValues(data: TimeSeries[], panel) {
|
export function updateLegendValues(data: TimeSeries[], panel, height) {
|
||||||
for (let i = 0; i < data.length; i++) {
|
for (let i = 0; i < data.length; i++) {
|
||||||
let series = data[i];
|
let series = data[i];
|
||||||
let yaxes = panel.yaxes;
|
const yaxes = panel.yaxes;
|
||||||
const seriesYAxis = series.yaxis || 1;
|
const seriesYAxis = series.yaxis || 1;
|
||||||
let axis = yaxes[seriesYAxis - 1];
|
const axis = yaxes[seriesYAxis - 1];
|
||||||
let { tickDecimals, scaledDecimals } = getFlotTickDecimals(data, axis);
|
let formater = kbn.valueFormats[axis.format];
|
||||||
let formater = kbn.valueFormats[panel.yaxes[seriesYAxis - 1].format];
|
|
||||||
|
|
||||||
// decimal override
|
// decimal override
|
||||||
if (_.isNumber(panel.decimals)) {
|
if (_.isNumber(panel.decimals)) {
|
||||||
series.updateLegendValues(formater, panel.decimals, null);
|
series.updateLegendValues(formater, panel.decimals, null);
|
||||||
|
} else if (_.isNumber(axis.decimals)) {
|
||||||
|
series.updateLegendValues(formater, axis.decimals + 1, null);
|
||||||
} else {
|
} else {
|
||||||
// auto decimals
|
// auto decimals
|
||||||
// legend and tooltip gets one more decimal precision
|
// legend and tooltip gets one more decimal precision
|
||||||
// than graph legend ticks
|
// than graph legend ticks
|
||||||
|
const { datamin, datamax } = getDataMinMax(data);
|
||||||
|
let { tickDecimals, scaledDecimals } = getFlotTickDecimals(datamin, datamax, axis, height);
|
||||||
tickDecimals = (tickDecimals || -1) + 1;
|
tickDecimals = (tickDecimals || -1) + 1;
|
||||||
series.updateLegendValues(formater, tickDecimals, scaledDecimals + 2);
|
series.updateLegendValues(formater, tickDecimals, scaledDecimals + 2);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { getDataMinMax } from 'app/core/time_series2';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate tick step.
|
* Calculate tick step.
|
||||||
* Implementation from d3-array (ticks.js)
|
* Implementation from d3-array (ticks.js)
|
||||||
@@ -121,12 +119,10 @@ export function getFlotRange(panelMin, panelMax, datamin, datamax) {
|
|||||||
* Calculate tick decimals.
|
* Calculate tick decimals.
|
||||||
* Implementation from Flot.
|
* Implementation from Flot.
|
||||||
*/
|
*/
|
||||||
export function getFlotTickDecimals(data, axis) {
|
export function getFlotTickDecimals(datamin, datamax, axis, height) {
|
||||||
let { datamin, datamax } = getDataMinMax(data);
|
const { min, max } = getFlotRange(axis.min, axis.max, datamin, datamax);
|
||||||
let { min, max } = getFlotRange(axis.min, axis.max, datamin, datamax);
|
const noTicks = 0.3 * Math.sqrt(height);
|
||||||
let noTicks = 3;
|
const delta = (max - min) / noTicks;
|
||||||
let tickDecimals, maxDec;
|
|
||||||
let delta = (max - min) / noTicks;
|
|
||||||
let dec = -Math.floor(Math.log(delta) / Math.LN10);
|
let dec = -Math.floor(Math.log(delta) / Math.LN10);
|
||||||
|
|
||||||
let magn = Math.pow(10, -dec);
|
let magn = Math.pow(10, -dec);
|
||||||
@@ -139,19 +135,17 @@ export function getFlotTickDecimals(data, axis) {
|
|||||||
} else if (norm < 3) {
|
} else if (norm < 3) {
|
||||||
size = 2;
|
size = 2;
|
||||||
// special case for 2.5, requires an extra decimal
|
// special case for 2.5, requires an extra decimal
|
||||||
if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) {
|
if (norm > 2.25) {
|
||||||
size = 2.5;
|
size = 2.5;
|
||||||
++dec;
|
|
||||||
}
|
}
|
||||||
} else if (norm < 7.5) {
|
} else if (norm < 7.5) {
|
||||||
size = 5;
|
size = 5;
|
||||||
} else {
|
} else {
|
||||||
size = 10;
|
size = 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
size *= magn;
|
size *= magn;
|
||||||
|
|
||||||
tickDecimals = Math.max(0, maxDec != null ? maxDec : dec);
|
const tickDecimals = Math.max(0, -Math.floor(Math.log(delta) / Math.LN10) + 1);
|
||||||
// grafana addition
|
// grafana addition
|
||||||
const scaledDecimals = tickDecimals - Math.floor(Math.log(size) / Math.LN10);
|
const scaledDecimals = tickDecimals - Math.floor(Math.log(size) / Math.LN10);
|
||||||
return { tickDecimals, scaledDecimals };
|
return { tickDecimals, scaledDecimals };
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ export class AdminEditUserCtrl {
|
|||||||
|
|
||||||
$scope.removeOrgUser = function(orgUser) {
|
$scope.removeOrgUser = function(orgUser) {
|
||||||
backendSrv.delete('/api/orgs/' + orgUser.orgId + '/users/' + $scope.user_id).then(function() {
|
backendSrv.delete('/api/orgs/' + orgUser.orgId + '/users/' + $scope.user_id).then(function() {
|
||||||
|
$scope.getUser($scope.user_id);
|
||||||
$scope.getUserOrgs($scope.user_id);
|
$scope.getUserOrgs($scope.user_id);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -108,6 +109,7 @@ export class AdminEditUserCtrl {
|
|||||||
$scope.newOrg.loginOrEmail = $scope.user.login;
|
$scope.newOrg.loginOrEmail = $scope.user.login;
|
||||||
|
|
||||||
backendSrv.post('/api/orgs/' + orgInfo.id + '/users/', $scope.newOrg).then(function() {
|
backendSrv.post('/api/orgs/' + orgInfo.id + '/users/', $scope.newOrg).then(function() {
|
||||||
|
$scope.getUser($scope.user_id);
|
||||||
$scope.getUserOrgs($scope.user_id);
|
$scope.getUserOrgs($scope.user_id);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,10 +22,10 @@ export class DashboardModel {
|
|||||||
editable: any;
|
editable: any;
|
||||||
graphTooltip: any;
|
graphTooltip: any;
|
||||||
time: any;
|
time: any;
|
||||||
originalTime: any;
|
private originalTime: any;
|
||||||
timepicker: any;
|
timepicker: any;
|
||||||
templating: any;
|
templating: any;
|
||||||
originalTemplating: any;
|
private originalTemplating: any;
|
||||||
annotations: any;
|
annotations: any;
|
||||||
refresh: any;
|
refresh: any;
|
||||||
snapshot: any;
|
snapshot: any;
|
||||||
@@ -50,6 +50,8 @@ export class DashboardModel {
|
|||||||
meta: true,
|
meta: true,
|
||||||
panels: true, // needs special handling
|
panels: true, // needs special handling
|
||||||
templating: true, // needs special handling
|
templating: true, // needs special handling
|
||||||
|
originalTime: true,
|
||||||
|
originalTemplating: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(data, meta?) {
|
constructor(data, meta?) {
|
||||||
@@ -70,12 +72,8 @@ export class DashboardModel {
|
|||||||
this.editable = data.editable !== false;
|
this.editable = data.editable !== false;
|
||||||
this.graphTooltip = data.graphTooltip || 0;
|
this.graphTooltip = data.graphTooltip || 0;
|
||||||
this.time = data.time || { from: 'now-6h', to: 'now' };
|
this.time = data.time || { from: 'now-6h', to: 'now' };
|
||||||
this.originalTime = _.cloneDeep(this.time);
|
|
||||||
this.timepicker = data.timepicker || {};
|
this.timepicker = data.timepicker || {};
|
||||||
this.templating = this.ensureListExist(data.templating);
|
this.templating = this.ensureListExist(data.templating);
|
||||||
this.originalTemplating = _.map(this.templating.list, variable => {
|
|
||||||
return { name: variable.name, current: _.clone(variable.current) };
|
|
||||||
});
|
|
||||||
this.annotations = this.ensureListExist(data.annotations);
|
this.annotations = this.ensureListExist(data.annotations);
|
||||||
this.refresh = data.refresh;
|
this.refresh = data.refresh;
|
||||||
this.snapshot = data.snapshot;
|
this.snapshot = data.snapshot;
|
||||||
@@ -85,6 +83,9 @@ export class DashboardModel {
|
|||||||
this.gnetId = data.gnetId || null;
|
this.gnetId = data.gnetId || null;
|
||||||
this.panels = _.map(data.panels || [], panelData => new PanelModel(panelData));
|
this.panels = _.map(data.panels || [], panelData => new PanelModel(panelData));
|
||||||
|
|
||||||
|
this.resetOriginalVariables();
|
||||||
|
this.resetOriginalTime();
|
||||||
|
|
||||||
this.initMeta(meta);
|
this.initMeta(meta);
|
||||||
this.updateSchema(data);
|
this.updateSchema(data);
|
||||||
|
|
||||||
@@ -138,8 +139,8 @@ export class DashboardModel {
|
|||||||
// cleans meta data and other non persistent state
|
// cleans meta data and other non persistent state
|
||||||
getSaveModelClone(options?) {
|
getSaveModelClone(options?) {
|
||||||
let defaults = _.defaults(options || {}, {
|
let defaults = _.defaults(options || {}, {
|
||||||
saveVariables: false,
|
saveVariables: true,
|
||||||
saveTimerange: false,
|
saveTimerange: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// make clone
|
// make clone
|
||||||
@@ -153,15 +154,23 @@ export class DashboardModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// get variable save models
|
// get variable save models
|
||||||
//console.log(this.templating.list);
|
|
||||||
copy.templating = {
|
copy.templating = {
|
||||||
list: _.map(this.templating.list, variable => (variable.getSaveModel ? variable.getSaveModel() : variable)),
|
list: _.map(this.templating.list, variable => (variable.getSaveModel ? variable.getSaveModel() : variable)),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!defaults.saveVariables && copy.templating.list.length === this.originalTemplating.length) {
|
if (!defaults.saveVariables) {
|
||||||
for (let i = 0; i < copy.templating.list.length; i++) {
|
for (let i = 0; i < copy.templating.list.length; i++) {
|
||||||
if (copy.templating.list[i].name === this.originalTemplating[i].name) {
|
let current = copy.templating.list[i];
|
||||||
copy.templating.list[i].current = this.originalTemplating[i].current;
|
let original = _.find(this.originalTemplating, { name: current.name, type: current.type });
|
||||||
|
|
||||||
|
if (!original) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current.type === 'adhoc') {
|
||||||
|
copy.templating.list[i].filters = original.filters;
|
||||||
|
} else {
|
||||||
|
copy.templating.list[i].current = original.current;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -785,4 +794,40 @@ export class DashboardModel {
|
|||||||
let migrator = new DashboardMigrator(this);
|
let migrator = new DashboardMigrator(this);
|
||||||
migrator.updateSchema(old);
|
migrator.updateSchema(old);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resetOriginalTime() {
|
||||||
|
this.originalTime = _.cloneDeep(this.time);
|
||||||
|
}
|
||||||
|
|
||||||
|
hasTimeChanged() {
|
||||||
|
return !_.isEqual(this.time, this.originalTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
resetOriginalVariables() {
|
||||||
|
this.originalTemplating = _.map(this.templating.list, variable => {
|
||||||
|
return {
|
||||||
|
name: variable.name,
|
||||||
|
type: variable.type,
|
||||||
|
current: _.cloneDeep(variable.current),
|
||||||
|
filters: _.cloneDeep(variable.filters),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
hasVariableValuesChanged() {
|
||||||
|
if (this.templating.list.length !== this.originalTemplating.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = _.map(this.templating.list, variable => {
|
||||||
|
return {
|
||||||
|
name: variable.name,
|
||||||
|
type: variable.type,
|
||||||
|
current: _.cloneDeep(variable.current),
|
||||||
|
filters: _.cloneDeep(variable.filters),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return !_.isEqual(updated, this.originalTemplating);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,8 +63,7 @@ export class DashboardExporter {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// check up panel data sources
|
const processPanel = panel => {
|
||||||
for (let panel of saveModel.panels) {
|
|
||||||
if (panel.datasource !== undefined) {
|
if (panel.datasource !== undefined) {
|
||||||
templateizeDatasourceUsage(panel);
|
templateizeDatasourceUsage(panel);
|
||||||
}
|
}
|
||||||
@@ -86,6 +85,18 @@ export class DashboardExporter {
|
|||||||
version: panelDef.info.version,
|
version: panelDef.info.version,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// check up panel data sources
|
||||||
|
for (let panel of saveModel.panels) {
|
||||||
|
processPanel(panel);
|
||||||
|
|
||||||
|
// handle collapsed rows
|
||||||
|
if (panel.collapsed !== undefined && panel.collapsed === true && panel.panels) {
|
||||||
|
for (let rowPanel of panel.panels) {
|
||||||
|
processPanel(rowPanel);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// templatize template vars
|
// templatize template vars
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import coreModule from 'app/core/core_module';
|
import coreModule from 'app/core/core_module';
|
||||||
import _ from 'lodash';
|
|
||||||
|
|
||||||
const template = `
|
const template = `
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
@@ -70,7 +69,6 @@ export class SaveDashboardModalCtrl {
|
|||||||
message: string;
|
message: string;
|
||||||
saveVariables = false;
|
saveVariables = false;
|
||||||
saveTimerange = false;
|
saveTimerange = false;
|
||||||
templating: any;
|
|
||||||
time: any;
|
time: any;
|
||||||
originalTime: any;
|
originalTime: any;
|
||||||
current = [];
|
current = [];
|
||||||
@@ -87,40 +85,8 @@ export class SaveDashboardModalCtrl {
|
|||||||
this.message = '';
|
this.message = '';
|
||||||
this.max = 64;
|
this.max = 64;
|
||||||
this.isSaving = false;
|
this.isSaving = false;
|
||||||
this.templating = dashboardSrv.dash.templating.list;
|
this.timeChange = this.dashboardSrv.getCurrent().hasTimeChanged();
|
||||||
|
this.variableValueChange = this.dashboardSrv.getCurrent().hasVariableValuesChanged();
|
||||||
this.compareTemplating();
|
|
||||||
this.compareTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
compareTime() {
|
|
||||||
if (_.isEqual(this.dashboardSrv.dash.time, this.dashboardSrv.dash.originalTime)) {
|
|
||||||
this.timeChange = false;
|
|
||||||
} else {
|
|
||||||
this.timeChange = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
compareTemplating() {
|
|
||||||
//checks if variables has been added or removed, if so variables will be saved automatically
|
|
||||||
if (this.dashboardSrv.dash.originalTemplating.length !== this.dashboardSrv.dash.templating.list.length) {
|
|
||||||
return (this.variableValueChange = false);
|
|
||||||
}
|
|
||||||
|
|
||||||
//checks if variable value has changed
|
|
||||||
if (this.dashboardSrv.dash.templating.list.length > 0) {
|
|
||||||
for (let i = 0; i < this.dashboardSrv.dash.templating.list.length; i++) {
|
|
||||||
if (
|
|
||||||
this.dashboardSrv.dash.templating.list[i].current.text !==
|
|
||||||
this.dashboardSrv.dash.originalTemplating[i].current.text
|
|
||||||
) {
|
|
||||||
return (this.variableValueChange = true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (this.variableValueChange = false);
|
|
||||||
} else {
|
|
||||||
return (this.variableValueChange = false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
save() {
|
save() {
|
||||||
@@ -139,7 +105,19 @@ export class SaveDashboardModalCtrl {
|
|||||||
|
|
||||||
this.isSaving = true;
|
this.isSaving = true;
|
||||||
|
|
||||||
return this.dashboardSrv.save(saveModel, options).then(this.dismiss);
|
return this.dashboardSrv.save(saveModel, options).then(this.postSave.bind(this, options));
|
||||||
|
}
|
||||||
|
|
||||||
|
postSave(options) {
|
||||||
|
if (options.saveVariables) {
|
||||||
|
this.dashboardSrv.getCurrent().resetOriginalVariables();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.saveTimerange) {
|
||||||
|
this.dashboardSrv.getCurrent().resetOriginalTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dismiss();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -123,6 +123,9 @@ export class ShareSnapshotCtrl {
|
|||||||
enable: annotation.enable,
|
enable: annotation.enable,
|
||||||
iconColor: annotation.iconColor,
|
iconColor: annotation.iconColor,
|
||||||
snapshotData: annotation.snapshotData,
|
snapshotData: annotation.snapshotData,
|
||||||
|
type: annotation.type,
|
||||||
|
builtIn: annotation.builtIn,
|
||||||
|
hide: annotation.hide,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.value();
|
.value();
|
||||||
|
|||||||
@@ -435,8 +435,67 @@ describe('DashboardModel', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('save variables and timeline', () => {
|
describe('Given model with time', () => {
|
||||||
let model;
|
let model: DashboardModel;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
model = new DashboardModel({
|
||||||
|
time: {
|
||||||
|
from: 'now-6h',
|
||||||
|
to: 'now',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(model.hasTimeChanged()).toBeFalsy();
|
||||||
|
model.time = {
|
||||||
|
from: 'now-3h',
|
||||||
|
to: 'now-1h',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hasTimeChanged should be true', () => {
|
||||||
|
expect(model.hasTimeChanged()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getSaveModelClone should return original time when saveTimerange=false', () => {
|
||||||
|
let options = { saveTimerange: false };
|
||||||
|
let saveModel = model.getSaveModelClone(options);
|
||||||
|
|
||||||
|
expect(saveModel.time.from).toBe('now-6h');
|
||||||
|
expect(saveModel.time.to).toBe('now');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getSaveModelClone should return updated time when saveTimerange=true', () => {
|
||||||
|
let options = { saveTimerange: true };
|
||||||
|
let saveModel = model.getSaveModelClone(options);
|
||||||
|
|
||||||
|
expect(saveModel.time.from).toBe('now-3h');
|
||||||
|
expect(saveModel.time.to).toBe('now-1h');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hasTimeChanged should be false when reset original time', () => {
|
||||||
|
model.resetOriginalTime();
|
||||||
|
expect(model.hasTimeChanged()).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getSaveModelClone should return original time when saveTimerange=false', () => {
|
||||||
|
let options = { saveTimerange: false };
|
||||||
|
let saveModel = model.getSaveModelClone(options);
|
||||||
|
|
||||||
|
expect(saveModel.time.from).toBe('now-6h');
|
||||||
|
expect(saveModel.time.to).toBe('now');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getSaveModelClone should return updated time when saveTimerange=true', () => {
|
||||||
|
let options = { saveTimerange: true };
|
||||||
|
let saveModel = model.getSaveModelClone(options);
|
||||||
|
|
||||||
|
expect(saveModel.time.from).toBe('now-3h');
|
||||||
|
expect(saveModel.time.to).toBe('now-1h');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Given model with template variable of type query', () => {
|
||||||
|
let model: DashboardModel;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
model = new DashboardModel({
|
model = new DashboardModel({
|
||||||
@@ -444,6 +503,7 @@ describe('DashboardModel', function() {
|
|||||||
list: [
|
list: [
|
||||||
{
|
{
|
||||||
name: 'Server',
|
name: 'Server',
|
||||||
|
type: 'query',
|
||||||
current: {
|
current: {
|
||||||
selected: true,
|
selected: true,
|
||||||
text: 'server_001',
|
text: 'server_001',
|
||||||
@@ -452,45 +512,127 @@ describe('DashboardModel', function() {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
time: {
|
|
||||||
from: 'now-6h',
|
|
||||||
to: 'now',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
model.templating.list[0] = {
|
expect(model.hasVariableValuesChanged()).toBeFalsy();
|
||||||
name: 'Server',
|
});
|
||||||
|
|
||||||
|
it('hasVariableValuesChanged should be false when adding a template variable', () => {
|
||||||
|
model.templating.list.push({
|
||||||
|
name: 'Server2',
|
||||||
|
type: 'query',
|
||||||
current: {
|
current: {
|
||||||
selected: true,
|
selected: true,
|
||||||
text: 'server_002',
|
text: 'server_002',
|
||||||
value: 'server_002',
|
value: 'server_002',
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
model.time = {
|
expect(model.hasVariableValuesChanged()).toBeFalsy();
|
||||||
from: 'now-3h',
|
|
||||||
to: 'now',
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not save variables and timeline', () => {
|
it('hasVariableValuesChanged should be false when removing existing template variable', () => {
|
||||||
let options = {
|
model.templating.list = [];
|
||||||
saveVariables: false,
|
expect(model.hasVariableValuesChanged()).toBeFalsy();
|
||||||
saveTimerange: false,
|
});
|
||||||
};
|
|
||||||
|
it('hasVariableValuesChanged should be true when changing value of template variable', () => {
|
||||||
|
model.templating.list[0].current.text = 'server_002';
|
||||||
|
expect(model.hasVariableValuesChanged()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getSaveModelClone should return original variable when saveVariables=false', () => {
|
||||||
|
model.templating.list[0].current.text = 'server_002';
|
||||||
|
|
||||||
|
let options = { saveVariables: false };
|
||||||
let saveModel = model.getSaveModelClone(options);
|
let saveModel = model.getSaveModelClone(options);
|
||||||
|
|
||||||
expect(saveModel.templating.list[0].current.text).toBe('server_001');
|
expect(saveModel.templating.list[0].current.text).toBe('server_001');
|
||||||
expect(saveModel.time.from).toBe('now-6h');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should save variables and timeline', () => {
|
it('getSaveModelClone should return updated variable when saveVariables=true', () => {
|
||||||
let options = {
|
model.templating.list[0].current.text = 'server_002';
|
||||||
saveVariables: true,
|
|
||||||
saveTimerange: true,
|
let options = { saveVariables: true };
|
||||||
};
|
|
||||||
let saveModel = model.getSaveModelClone(options);
|
let saveModel = model.getSaveModelClone(options);
|
||||||
|
|
||||||
expect(saveModel.templating.list[0].current.text).toBe('server_002');
|
expect(saveModel.templating.list[0].current.text).toBe('server_002');
|
||||||
expect(saveModel.time.from).toBe('now-3h');
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Given model with template variable of type adhoc', () => {
|
||||||
|
let model: DashboardModel;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
model = new DashboardModel({
|
||||||
|
templating: {
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
name: 'Filter',
|
||||||
|
type: 'adhoc',
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
key: '@hostname',
|
||||||
|
operator: '=',
|
||||||
|
value: 'server 20',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(model.hasVariableValuesChanged()).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hasVariableValuesChanged should be false when adding a template variable', () => {
|
||||||
|
model.templating.list.push({
|
||||||
|
name: 'Filter',
|
||||||
|
type: 'adhoc',
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
key: '@hostname',
|
||||||
|
operator: '=',
|
||||||
|
value: 'server 1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(model.hasVariableValuesChanged()).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hasVariableValuesChanged should be false when removing existing template variable', () => {
|
||||||
|
model.templating.list = [];
|
||||||
|
expect(model.hasVariableValuesChanged()).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hasVariableValuesChanged should be true when changing value of filter', () => {
|
||||||
|
model.templating.list[0].filters[0].value = 'server 1';
|
||||||
|
expect(model.hasVariableValuesChanged()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hasVariableValuesChanged should be true when adding an additional condition', () => {
|
||||||
|
model.templating.list[0].filters[0].condition = 'AND';
|
||||||
|
model.templating.list[0].filters[1] = {
|
||||||
|
key: '@metric',
|
||||||
|
operator: '=',
|
||||||
|
value: 'logins.count',
|
||||||
|
};
|
||||||
|
expect(model.hasVariableValuesChanged()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getSaveModelClone should return original variable when saveVariables=false', () => {
|
||||||
|
model.templating.list[0].filters[0].value = 'server 1';
|
||||||
|
|
||||||
|
let options = { saveVariables: false };
|
||||||
|
let saveModel = model.getSaveModelClone(options);
|
||||||
|
|
||||||
|
expect(saveModel.templating.list[0].filters[0].value).toBe('server 20');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getSaveModelClone should return updated variable when saveVariables=true', () => {
|
||||||
|
model.templating.list[0].filters[0].value = 'server 1';
|
||||||
|
|
||||||
|
let options = { saveVariables: true };
|
||||||
|
let saveModel = model.getSaveModelClone(options);
|
||||||
|
|
||||||
|
expect(saveModel.templating.list[0].filters[0].value).toBe('server 1');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -62,6 +62,27 @@ describe('given dashboard with repeated panels', () => {
|
|||||||
type: 'graph',
|
type: 'graph',
|
||||||
},
|
},
|
||||||
{ id: 3, repeat: null, repeatPanelId: 2 },
|
{ id: 3, repeat: null, repeatPanelId: 2 },
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
collapsed: true,
|
||||||
|
panels: [
|
||||||
|
{ id: 10, datasource: 'gfdb', type: 'table' },
|
||||||
|
{ id: 11 },
|
||||||
|
{
|
||||||
|
id: 12,
|
||||||
|
datasource: '-- Mixed --',
|
||||||
|
targets: [{ datasource: 'other' }],
|
||||||
|
},
|
||||||
|
{ id: 13, datasource: '$ds' },
|
||||||
|
{
|
||||||
|
id: 14,
|
||||||
|
repeat: 'apps',
|
||||||
|
datasource: 'gfdb',
|
||||||
|
type: 'heatmap',
|
||||||
|
},
|
||||||
|
{ id: 15, repeat: null, repeatPanelId: 14 },
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -78,6 +99,18 @@ describe('given dashboard with repeated panels', () => {
|
|||||||
info: { version: '1.1.0' },
|
info: { version: '1.1.0' },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
config.panels['table'] = {
|
||||||
|
id: 'table',
|
||||||
|
name: 'Table',
|
||||||
|
info: { version: '1.1.1' },
|
||||||
|
};
|
||||||
|
|
||||||
|
config.panels['heatmap'] = {
|
||||||
|
id: 'heatmap',
|
||||||
|
name: 'Heatmap',
|
||||||
|
info: { version: '1.1.2' },
|
||||||
|
};
|
||||||
|
|
||||||
dash = new DashboardModel(dash, {});
|
dash = new DashboardModel(dash, {});
|
||||||
var exporter = new DashboardExporter(datasourceSrvStub);
|
var exporter = new DashboardExporter(datasourceSrvStub);
|
||||||
exporter.makeExportable(dash).then(clean => {
|
exporter.makeExportable(dash).then(clean => {
|
||||||
@@ -91,6 +124,11 @@ describe('given dashboard with repeated panels', () => {
|
|||||||
expect(panel.datasource).toBe('${DS_GFDB}');
|
expect(panel.datasource).toBe('${DS_GFDB}');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should replace datasource refs in collapsed row', () => {
|
||||||
|
var panel = exported.panels[5].panels[0];
|
||||||
|
expect(panel.datasource).toBe('${DS_GFDB}');
|
||||||
|
});
|
||||||
|
|
||||||
it('should replace datasource in variable query', () => {
|
it('should replace datasource in variable query', () => {
|
||||||
expect(exported.templating.list[0].datasource).toBe('${DS_GFDB}');
|
expect(exported.templating.list[0].datasource).toBe('${DS_GFDB}');
|
||||||
expect(exported.templating.list[0].options.length).toBe(0);
|
expect(exported.templating.list[0].options.length).toBe(0);
|
||||||
@@ -126,13 +164,27 @@ describe('given dashboard with repeated panels', () => {
|
|||||||
expect(require).not.toBe(undefined);
|
expect(require).not.toBe(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add panel to required', () => {
|
it('should add graph panel to required', () => {
|
||||||
var require = _.find(exported.__requires, { name: 'Graph' });
|
var require = _.find(exported.__requires, { name: 'Graph' });
|
||||||
expect(require.name).toBe('Graph');
|
expect(require.name).toBe('Graph');
|
||||||
expect(require.id).toBe('graph');
|
expect(require.id).toBe('graph');
|
||||||
expect(require.version).toBe('1.1.0');
|
expect(require.version).toBe('1.1.0');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should add table panel to required', () => {
|
||||||
|
var require = _.find(exported.__requires, { name: 'Table' });
|
||||||
|
expect(require.name).toBe('Table');
|
||||||
|
expect(require.id).toBe('table');
|
||||||
|
expect(require.version).toBe('1.1.1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add heatmap panel to required', () => {
|
||||||
|
var require = _.find(exported.__requires, { name: 'Heatmap' });
|
||||||
|
expect(require.name).toBe('Heatmap');
|
||||||
|
expect(require.id).toBe('heatmap');
|
||||||
|
expect(require.version).toBe('1.1.2');
|
||||||
|
});
|
||||||
|
|
||||||
it('should add grafana version', () => {
|
it('should add grafana version', () => {
|
||||||
var require = _.find(exported.__requires, { name: 'Grafana' });
|
var require = _.find(exported.__requires, { name: 'Grafana' });
|
||||||
expect(require.type).toBe('grafana');
|
expect(require.type).toBe('grafana');
|
||||||
|
|||||||
@@ -1,128 +1,57 @@
|
|||||||
import { SaveDashboardModalCtrl } from '../save_modal';
|
import { SaveDashboardModalCtrl } from '../save_modal';
|
||||||
|
|
||||||
jest.mock('app/core/services/context_srv', () => ({}));
|
const setup = (timeChanged, variableValuesChanged, cb) => {
|
||||||
|
const dash = {
|
||||||
|
hasTimeChanged: jest.fn().mockReturnValue(timeChanged),
|
||||||
|
hasVariableValuesChanged: jest.fn().mockReturnValue(variableValuesChanged),
|
||||||
|
resetOriginalTime: jest.fn(),
|
||||||
|
resetOriginalVariables: jest.fn(),
|
||||||
|
getSaveModelClone: jest.fn().mockReturnValue({}),
|
||||||
|
};
|
||||||
|
const dashboardSrvMock = {
|
||||||
|
getCurrent: jest.fn().mockReturnValue(dash),
|
||||||
|
save: jest.fn().mockReturnValue(Promise.resolve()),
|
||||||
|
};
|
||||||
|
const ctrl = new SaveDashboardModalCtrl(dashboardSrvMock);
|
||||||
|
ctrl.saveForm = {
|
||||||
|
$valid: true,
|
||||||
|
};
|
||||||
|
ctrl.dismiss = () => Promise.resolve();
|
||||||
|
cb(dash, ctrl, dashboardSrvMock);
|
||||||
|
};
|
||||||
|
|
||||||
describe('SaveDashboardModal', () => {
|
describe('SaveDashboardModal', () => {
|
||||||
describe('save modal checkboxes', () => {
|
describe('Given time and template variable values have not changed', () => {
|
||||||
it('should show checkboxes', () => {
|
setup(false, false, (dash, ctrl: SaveDashboardModalCtrl) => {
|
||||||
let fakeDashboardSrv = {
|
it('When creating ctrl should set time and template variable values changed', () => {
|
||||||
dash: {
|
expect(ctrl.timeChange).toBeFalsy();
|
||||||
templating: {
|
expect(ctrl.variableValueChange).toBeFalsy();
|
||||||
list: [
|
});
|
||||||
{
|
|
||||||
current: {
|
|
||||||
selected: true,
|
|
||||||
tags: Array(0),
|
|
||||||
text: 'server_001',
|
|
||||||
value: 'server_001',
|
|
||||||
},
|
|
||||||
name: 'Server',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
originalTemplating: [
|
|
||||||
{
|
|
||||||
current: {
|
|
||||||
selected: true,
|
|
||||||
text: 'server_002',
|
|
||||||
value: 'server_002',
|
|
||||||
},
|
|
||||||
name: 'Server',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
time: {
|
|
||||||
from: 'now-3h',
|
|
||||||
to: 'now',
|
|
||||||
},
|
|
||||||
originalTime: {
|
|
||||||
from: 'now-6h',
|
|
||||||
to: 'now',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
let modal = new SaveDashboardModalCtrl(fakeDashboardSrv);
|
|
||||||
|
|
||||||
expect(modal.timeChange).toBe(true);
|
|
||||||
expect(modal.variableValueChange).toBe(true);
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should hide checkboxes', () => {
|
describe('Given time and template variable values have changed', () => {
|
||||||
let fakeDashboardSrv = {
|
setup(true, true, (dash, ctrl: SaveDashboardModalCtrl) => {
|
||||||
dash: {
|
it('When creating ctrl should set time and template variable values changed', () => {
|
||||||
templating: {
|
expect(ctrl.timeChange).toBeTruthy();
|
||||||
list: [
|
expect(ctrl.variableValueChange).toBeTruthy();
|
||||||
{
|
});
|
||||||
current: {
|
|
||||||
selected: true,
|
|
||||||
text: 'server_002',
|
|
||||||
value: 'server_002',
|
|
||||||
},
|
|
||||||
name: 'Server',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
originalTemplating: [
|
|
||||||
{
|
|
||||||
current: {
|
|
||||||
selected: true,
|
|
||||||
text: 'server_002',
|
|
||||||
value: 'server_002',
|
|
||||||
},
|
|
||||||
name: 'Server',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
time: {
|
|
||||||
from: 'now-3h',
|
|
||||||
to: 'now',
|
|
||||||
},
|
|
||||||
originalTime: {
|
|
||||||
from: 'now-3h',
|
|
||||||
to: 'now',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
let modal = new SaveDashboardModalCtrl(fakeDashboardSrv);
|
|
||||||
expect(modal.timeChange).toBe(false);
|
|
||||||
expect(modal.variableValueChange).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should hide variable checkboxes', () => {
|
it('When save time and variable value changes disabled and saving should reset original time and template variable values', async () => {
|
||||||
let fakeDashboardSrv = {
|
ctrl.saveTimerange = false;
|
||||||
dash: {
|
ctrl.saveVariables = false;
|
||||||
templating: {
|
await ctrl.save();
|
||||||
list: [
|
expect(dash.resetOriginalTime).toHaveBeenCalledTimes(0);
|
||||||
{
|
expect(dash.resetOriginalVariables).toHaveBeenCalledTimes(0);
|
||||||
current: {
|
});
|
||||||
selected: true,
|
|
||||||
text: 'server_002',
|
it('When save time and variable value changes enabled and saving should reset original time and template variable values', async () => {
|
||||||
value: 'server_002',
|
ctrl.saveTimerange = true;
|
||||||
},
|
ctrl.saveVariables = true;
|
||||||
name: 'Server',
|
await ctrl.save();
|
||||||
},
|
expect(dash.resetOriginalTime).toHaveBeenCalledTimes(1);
|
||||||
{
|
expect(dash.resetOriginalVariables).toHaveBeenCalledTimes(1);
|
||||||
current: {
|
});
|
||||||
selected: true,
|
|
||||||
text: 'web_002',
|
|
||||||
value: 'web_002',
|
|
||||||
},
|
|
||||||
name: 'Web',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
originalTemplating: [
|
|
||||||
{
|
|
||||||
current: {
|
|
||||||
selected: true,
|
|
||||||
text: 'server_002',
|
|
||||||
value: 'server_002',
|
|
||||||
},
|
|
||||||
name: 'Server',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
let modal = new SaveDashboardModalCtrl(fakeDashboardSrv);
|
|
||||||
expect(modal.variableValueChange).toBe(false);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -48,9 +48,11 @@ function dashLink($compile, $sanitize, linkSrv) {
|
|||||||
function update() {
|
function update() {
|
||||||
var linkInfo = linkSrv.getAnchorInfo(link);
|
var linkInfo = linkSrv.getAnchorInfo(link);
|
||||||
span.text(linkInfo.title);
|
span.text(linkInfo.title);
|
||||||
anchor.attr('href', linkInfo.href);
|
if (!link.asDropdown) {
|
||||||
sanitizeAnchor();
|
anchor.attr('href', linkInfo.href);
|
||||||
|
sanitizeAnchor();
|
||||||
|
}
|
||||||
|
elem.find('a').attr('data-placement', 'bottom');
|
||||||
// tooltip
|
// tooltip
|
||||||
elem.find('a').tooltip({
|
elem.find('a').tooltip({
|
||||||
title: $sanitize(scope.link.tooltip),
|
title: $sanitize(scope.link.tooltip),
|
||||||
|
|||||||
@@ -64,7 +64,8 @@ function graphDirective(timeSrv, popoverSrv, contextSrv) {
|
|||||||
}
|
}
|
||||||
annotations = ctrl.annotations || [];
|
annotations = ctrl.annotations || [];
|
||||||
buildFlotPairs(data);
|
buildFlotPairs(data);
|
||||||
updateLegendValues(data, panel);
|
const graphHeight = elem.height();
|
||||||
|
updateLegendValues(data, panel, graphHeight);
|
||||||
|
|
||||||
ctrl.events.emit('render-legend');
|
ctrl.events.emit('render-legend');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,4 +6,7 @@ gpg --allow-secret-key-import --import ~/private-repo/signing/private.key
|
|||||||
|
|
||||||
cp ./scripts/build/rpmmacros ~/.rpmmacros
|
cp ./scripts/build/rpmmacros ~/.rpmmacros
|
||||||
|
|
||||||
./scripts/build/sign_expect $GPG_KEY_PASSWORD dist/*.rpm
|
for package in dist/*.rpm; do
|
||||||
|
[ -e "$package" ] || continue
|
||||||
|
./scripts/build/sign_expect $GPG_KEY_PASSWORD $package
|
||||||
|
done
|
||||||
|
|||||||
Reference in New Issue
Block a user