Compare commits

...

17 Commits

Author SHA1 Message Date
Daniel Lee
30c882c18d Merge pull request #12364 from grafana/cp-5.2.0-beta3
Cherry-picks for v5.2.0-beta3
2018-06-21 11:11:12 +02:00
Marcus Efraimsson
e5836064ce release v5.2.0-beta3 2018-06-21 10:54:24 +02:00
Marcus Efraimsson
f7cb827944 build: fix signing of multiple rpm packages
(cherry picked from commit e617e23927)
2018-06-21 10:54:04 +02:00
Marcus Efraimsson
f76cafa68e Merge pull request #12357 from grafana/cp-5.2.0-beta2
Cherry-picks for v5.2.0-beta2
2018-06-20 15:21:50 +02:00
Marcus Efraimsson
cdae9126ed release v5.2.0-beta2 2018-06-20 14:58:30 +02:00
Marcus Efraimsson
b4c1df11f6 make sure to process panels in collapsed rows when exporting dashboard
(cherry picked from commit a2e08dc4e8)
2018-06-20 14:56:13 +02:00
Alexander Zobnin
7c94d5cd1a graph: fix legend decimals precision calculation
(cherry picked from commit 24f6d34abd)
2018-06-20 14:55:38 +02:00
Marcus Efraimsson
74d6b5fc1c dashboard: fix drop down links
(cherry picked from commit 4ef4a4d3a7)
2018-06-20 14:55:04 +02:00
Marcus Efraimsson
af42e0836a fix regressions after save modal changes of not storing time and variables per default
Fix problem with adhoc variable filters not handled.
Fix problem with saving variables and time per default when saving a
dashboard as/first time.
Fix updating dashboard model after save with saving time/variables
enabled so that next time you save you won't get checkboxes for save
time/variables unless any values changed.
Tests validating correctness if time/variable values has changed.

(cherry picked from commit 41ac8d4cd5)
2018-06-20 14:54:13 +02:00
Martin Molnar
f453fbe8ef feat(ldap): Allow use of DN in user attribute filter (#3132)
(cherry picked from commit be2fa54459)
2018-06-20 14:53:36 +02:00
Marcus Efraimsson
8d635efda0 snapshot: copy correct props when creating a snapshot
(cherry picked from commit a738347957)
2018-06-20 14:52:59 +02:00
Marcus Efraimsson
0f2e879339 set current org when adding/removing user to org
To not get into a situation where a user has a current organization assign which he is
not a member of we try to always make sure that a user has a valid current organization
assigned.

(cherry picked from commit 6d48d0a80c)
2018-06-20 14:52:11 +02:00
Marcus Efraimsson
e51dd88260 Merge pull request #12340 from grafana/apikey-permission-fix-cherry-pick2
v5.2.x cherry pick fix
2018-06-19 12:37:26 +02:00
Torkel Ödegaard
984293cc52 fix: fixed permission issue with api key with viewer role in dashboards with default permissions
(cherry picked from commit 24d0b43e62)
2018-06-19 11:14:33 +02:00
Marcus Efraimsson
9a1a9584b7 Merge pull request #12316 from grafana/v52_merge_master
Merge master to v5.2.x release branch
2018-06-18 11:50:48 +02:00
Marcus Efraimsson
8a69ffb007 Merge branch 'master' into v52_merge_master 2018-06-18 09:44:39 +02:00
Leonard Gram
faa5e699d2 Release v5.2.0-beta1. 2018-06-05 10:43:14 +02:00
24 changed files with 592 additions and 239 deletions

View File

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

View File

@@ -308,6 +308,10 @@ func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) {
} else {
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)
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 {
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
}

View File

@@ -83,7 +83,7 @@ func (g *dashboardGuardianImpl) checkAcl(permission m.PermissionType, acl []*m.D
for _, p := range acl {
// user match
if !g.user.IsAnonymous {
if !g.user.IsAnonymous && p.UserId > 0 {
if p.UserId == g.user.UserId && p.Permission >= permission {
return true, nil
}

View File

@@ -162,6 +162,11 @@ func TestGuardianViewer(t *testing.T) {
sc.parentFolderPermissionScenario(VIEWER, m.PERMISSION_EDIT, EDITOR_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
}
if sc.expectedFlags&actualFlag != sc.expectedFlags {
if actualFlag&sc.expectedFlags != actualFlag {
sc.reportFailure(tc, sc.expectedFlags.String(), actualFlag.String())
}

View File

@@ -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) {
bus.ClearBusHandlers()

View File

@@ -150,7 +150,7 @@ func TestAccountDataAccess(t *testing.T) {
})
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)
So(err, ShouldBeNil)
@@ -159,13 +159,25 @@ func TestAccountDataAccess(t *testing.T) {
err := GetSignedInUser(&query)
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.Name, ShouldEqual, "ac2 name")
So(query.Result.Login, ShouldEqual, "ac2")
So(query.Result.OrgName, ShouldEqual, "ac1@test.com")
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() {

View File

@@ -20,7 +20,14 @@ func init() {
func AddOrgUser(cmd *m.AddOrgUserCommand) error {
return inTransaction(func(sess *DBSession) error {
// 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
} else if len(res) == 1 {
return m.ErrOrgUserAlreadyAdded
@@ -41,7 +48,26 @@ func AddOrgUser(cmd *m.AddOrgUserCommand) error {
}
_, 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 {
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{
"DELETE FROM org_user 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)
})
}

View File

@@ -290,16 +290,20 @@ func SetUsingOrg(cmd *m.SetUsingOrgCommand) error {
}
return inTransaction(func(sess *DBSession) error {
user := m.User{
Id: cmd.UserId,
OrgId: cmd.OrgId,
}
_, err := sess.Id(cmd.UserId).Update(&user)
return err
return setUsingOrgInTransaction(sess, cmd.UserId, cmd.OrgId)
})
}
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 {
var user m.User
has, err := x.Id(query.UserId).Get(&user)

View File

@@ -96,33 +96,33 @@ func TestUserDataAccess(t *testing.T) {
})
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)
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)
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)
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)
Convey("Should delete connected org users and permissions", func() {
query := &m.GetOrgUsersQuery{OrgId: 1}
query := &m.GetOrgUsersQuery{OrgId: users[0].OrgId}
err = GetOrgUsersForTest(query)
So(err, ShouldBeNil)
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)
So(err, ShouldBeNil)
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)
So(err, ShouldBeNil)

View 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);
});
});
});

View File

@@ -1,4 +1,5 @@
import TimeSeries from 'app/core/time_series2';
import { updateLegendValues } from 'app/core/time_series2';
describe('TimeSeries', function() {
var points, series;
@@ -311,4 +312,55 @@ describe('TimeSeries', function() {
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);
});
});
});

View File

@@ -23,23 +23,27 @@ function translateFillOption(fill) {
* Calculate decimals for legend and update values for each series.
* @param data series data
* @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++) {
let series = data[i];
let yaxes = panel.yaxes;
const yaxes = panel.yaxes;
const seriesYAxis = series.yaxis || 1;
let axis = yaxes[seriesYAxis - 1];
let { tickDecimals, scaledDecimals } = getFlotTickDecimals(data, axis);
let formater = kbn.valueFormats[panel.yaxes[seriesYAxis - 1].format];
const axis = yaxes[seriesYAxis - 1];
let formater = kbn.valueFormats[axis.format];
// decimal override
if (_.isNumber(panel.decimals)) {
series.updateLegendValues(formater, panel.decimals, null);
} else if (_.isNumber(axis.decimals)) {
series.updateLegendValues(formater, axis.decimals + 1, null);
} else {
// auto decimals
// legend and tooltip gets one more decimal precision
// than graph legend ticks
const { datamin, datamax } = getDataMinMax(data);
let { tickDecimals, scaledDecimals } = getFlotTickDecimals(datamin, datamax, axis, height);
tickDecimals = (tickDecimals || -1) + 1;
series.updateLegendValues(formater, tickDecimals, scaledDecimals + 2);
}

View File

@@ -1,5 +1,3 @@
import { getDataMinMax } from 'app/core/time_series2';
/**
* Calculate tick step.
* Implementation from d3-array (ticks.js)
@@ -121,12 +119,10 @@ export function getFlotRange(panelMin, panelMax, datamin, datamax) {
* Calculate tick decimals.
* Implementation from Flot.
*/
export function getFlotTickDecimals(data, axis) {
let { datamin, datamax } = getDataMinMax(data);
let { min, max } = getFlotRange(axis.min, axis.max, datamin, datamax);
let noTicks = 3;
let tickDecimals, maxDec;
let delta = (max - min) / noTicks;
export function getFlotTickDecimals(datamin, datamax, axis, height) {
const { min, max } = getFlotRange(axis.min, axis.max, datamin, datamax);
const noTicks = 0.3 * Math.sqrt(height);
const delta = (max - min) / noTicks;
let dec = -Math.floor(Math.log(delta) / Math.LN10);
let magn = Math.pow(10, -dec);
@@ -139,19 +135,17 @@ export function getFlotTickDecimals(data, axis) {
} else if (norm < 3) {
size = 2;
// 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;
++dec;
}
} else if (norm < 7.5) {
size = 5;
} else {
size = 10;
}
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
const scaledDecimals = tickDecimals - Math.floor(Math.log(size) / Math.LN10);
return { tickDecimals, scaledDecimals };

View File

@@ -75,6 +75,7 @@ export class AdminEditUserCtrl {
$scope.removeOrgUser = function(orgUser) {
backendSrv.delete('/api/orgs/' + orgUser.orgId + '/users/' + $scope.user_id).then(function() {
$scope.getUser($scope.user_id);
$scope.getUserOrgs($scope.user_id);
});
};
@@ -108,6 +109,7 @@ export class AdminEditUserCtrl {
$scope.newOrg.loginOrEmail = $scope.user.login;
backendSrv.post('/api/orgs/' + orgInfo.id + '/users/', $scope.newOrg).then(function() {
$scope.getUser($scope.user_id);
$scope.getUserOrgs($scope.user_id);
});
};

View File

@@ -22,10 +22,10 @@ export class DashboardModel {
editable: any;
graphTooltip: any;
time: any;
originalTime: any;
private originalTime: any;
timepicker: any;
templating: any;
originalTemplating: any;
private originalTemplating: any;
annotations: any;
refresh: any;
snapshot: any;
@@ -50,6 +50,8 @@ export class DashboardModel {
meta: true,
panels: true, // needs special handling
templating: true, // needs special handling
originalTime: true,
originalTemplating: true,
};
constructor(data, meta?) {
@@ -70,12 +72,8 @@ export class DashboardModel {
this.editable = data.editable !== false;
this.graphTooltip = data.graphTooltip || 0;
this.time = data.time || { from: 'now-6h', to: 'now' };
this.originalTime = _.cloneDeep(this.time);
this.timepicker = data.timepicker || {};
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.refresh = data.refresh;
this.snapshot = data.snapshot;
@@ -85,6 +83,9 @@ export class DashboardModel {
this.gnetId = data.gnetId || null;
this.panels = _.map(data.panels || [], panelData => new PanelModel(panelData));
this.resetOriginalVariables();
this.resetOriginalTime();
this.initMeta(meta);
this.updateSchema(data);
@@ -138,8 +139,8 @@ export class DashboardModel {
// cleans meta data and other non persistent state
getSaveModelClone(options?) {
let defaults = _.defaults(options || {}, {
saveVariables: false,
saveTimerange: false,
saveVariables: true,
saveTimerange: true,
});
// make clone
@@ -153,15 +154,23 @@ export class DashboardModel {
}
// get variable save models
//console.log(this.templating.list);
copy.templating = {
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++) {
if (copy.templating.list[i].name === this.originalTemplating[i].name) {
copy.templating.list[i].current = this.originalTemplating[i].current;
let current = copy.templating.list[i];
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);
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);
}
}

View File

@@ -63,8 +63,7 @@ export class DashboardExporter {
);
};
// check up panel data sources
for (let panel of saveModel.panels) {
const processPanel = panel => {
if (panel.datasource !== undefined) {
templateizeDatasourceUsage(panel);
}
@@ -86,6 +85,18 @@ export class DashboardExporter {
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

View File

@@ -1,5 +1,4 @@
import coreModule from 'app/core/core_module';
import _ from 'lodash';
const template = `
<div class="modal-body">
@@ -70,7 +69,6 @@ export class SaveDashboardModalCtrl {
message: string;
saveVariables = false;
saveTimerange = false;
templating: any;
time: any;
originalTime: any;
current = [];
@@ -87,40 +85,8 @@ export class SaveDashboardModalCtrl {
this.message = '';
this.max = 64;
this.isSaving = false;
this.templating = dashboardSrv.dash.templating.list;
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);
}
this.timeChange = this.dashboardSrv.getCurrent().hasTimeChanged();
this.variableValueChange = this.dashboardSrv.getCurrent().hasVariableValuesChanged();
}
save() {
@@ -139,7 +105,19 @@ export class SaveDashboardModalCtrl {
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();
}
}

View File

@@ -123,6 +123,9 @@ export class ShareSnapshotCtrl {
enable: annotation.enable,
iconColor: annotation.iconColor,
snapshotData: annotation.snapshotData,
type: annotation.type,
builtIn: annotation.builtIn,
hide: annotation.hide,
};
})
.value();

View File

@@ -435,8 +435,67 @@ describe('DashboardModel', function() {
});
});
describe('save variables and timeline', () => {
let model;
describe('Given model with time', () => {
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(() => {
model = new DashboardModel({
@@ -444,6 +503,7 @@ describe('DashboardModel', function() {
list: [
{
name: 'Server',
type: 'query',
current: {
selected: true,
text: 'server_001',
@@ -452,45 +512,127 @@ describe('DashboardModel', function() {
},
],
},
time: {
from: 'now-6h',
to: 'now',
},
});
model.templating.list[0] = {
name: 'Server',
expect(model.hasVariableValuesChanged()).toBeFalsy();
});
it('hasVariableValuesChanged should be false when adding a template variable', () => {
model.templating.list.push({
name: 'Server2',
type: 'query',
current: {
selected: true,
text: 'server_002',
value: 'server_002',
},
};
model.time = {
from: 'now-3h',
to: 'now',
};
});
expect(model.hasVariableValuesChanged()).toBeFalsy();
});
it('should not save variables and timeline', () => {
let options = {
saveVariables: false,
saveTimerange: false,
};
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 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);
expect(saveModel.templating.list[0].current.text).toBe('server_001');
expect(saveModel.time.from).toBe('now-6h');
});
it('should save variables and timeline', () => {
let options = {
saveVariables: true,
saveTimerange: true,
};
it('getSaveModelClone should return updated variable when saveVariables=true', () => {
model.templating.list[0].current.text = 'server_002';
let options = { saveVariables: true };
let saveModel = model.getSaveModelClone(options);
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');
});
});
});

View File

@@ -62,6 +62,27 @@ describe('given dashboard with repeated panels', () => {
type: 'graph',
},
{ 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' },
};
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, {});
var exporter = new DashboardExporter(datasourceSrvStub);
exporter.makeExportable(dash).then(clean => {
@@ -91,6 +124,11 @@ describe('given dashboard with repeated panels', () => {
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', () => {
expect(exported.templating.list[0].datasource).toBe('${DS_GFDB}');
expect(exported.templating.list[0].options.length).toBe(0);
@@ -126,13 +164,27 @@ describe('given dashboard with repeated panels', () => {
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' });
expect(require.name).toBe('Graph');
expect(require.id).toBe('graph');
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', () => {
var require = _.find(exported.__requires, { name: 'Grafana' });
expect(require.type).toBe('grafana');

View File

@@ -1,128 +1,57 @@
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('save modal checkboxes', () => {
it('should show checkboxes', () => {
let fakeDashboardSrv = {
dash: {
templating: {
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);
describe('Given time and template variable values have not changed', () => {
setup(false, false, (dash, ctrl: SaveDashboardModalCtrl) => {
it('When creating ctrl should set time and template variable values changed', () => {
expect(ctrl.timeChange).toBeFalsy();
expect(ctrl.variableValueChange).toBeFalsy();
});
});
});
it('should hide checkboxes', () => {
let fakeDashboardSrv = {
dash: {
templating: {
list: [
{
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);
});
describe('Given time and template variable values have changed', () => {
setup(true, true, (dash, ctrl: SaveDashboardModalCtrl) => {
it('When creating ctrl should set time and template variable values changed', () => {
expect(ctrl.timeChange).toBeTruthy();
expect(ctrl.variableValueChange).toBeTruthy();
});
it('should hide variable checkboxes', () => {
let fakeDashboardSrv = {
dash: {
templating: {
list: [
{
current: {
selected: true,
text: 'server_002',
value: 'server_002',
},
name: 'Server',
},
{
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);
it('When save time and variable value changes disabled and saving should reset original time and template variable values', async () => {
ctrl.saveTimerange = false;
ctrl.saveVariables = false;
await ctrl.save();
expect(dash.resetOriginalTime).toHaveBeenCalledTimes(0);
expect(dash.resetOriginalVariables).toHaveBeenCalledTimes(0);
});
it('When save time and variable value changes enabled and saving should reset original time and template variable values', async () => {
ctrl.saveTimerange = true;
ctrl.saveVariables = true;
await ctrl.save();
expect(dash.resetOriginalTime).toHaveBeenCalledTimes(1);
expect(dash.resetOriginalVariables).toHaveBeenCalledTimes(1);
});
});
});
});

View File

@@ -48,9 +48,11 @@ function dashLink($compile, $sanitize, linkSrv) {
function update() {
var linkInfo = linkSrv.getAnchorInfo(link);
span.text(linkInfo.title);
anchor.attr('href', linkInfo.href);
sanitizeAnchor();
if (!link.asDropdown) {
anchor.attr('href', linkInfo.href);
sanitizeAnchor();
}
elem.find('a').attr('data-placement', 'bottom');
// tooltip
elem.find('a').tooltip({
title: $sanitize(scope.link.tooltip),

View File

@@ -64,7 +64,8 @@ function graphDirective(timeSrv, popoverSrv, contextSrv) {
}
annotations = ctrl.annotations || [];
buildFlotPairs(data);
updateLegendValues(data, panel);
const graphHeight = elem.height();
updateLegendValues(data, panel, graphHeight);
ctrl.events.emit('render-legend');
});

View File

@@ -6,4 +6,7 @@ gpg --allow-secret-key-import --import ~/private-repo/signing/private.key
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