save modal ux improvements (#11822)

changes to save modal when saving an updated dashboard

Changed time range and variables are now not saved by default, 
you'll need to actively choose if you want to save updated time 
range and or variables.
This commit is contained in:
Patrick O'Carroll 2018-05-31 15:40:57 +02:00 committed by Marcus Efraimsson
parent 938deae4b4
commit 37f9bdfc8c
4 changed files with 233 additions and 7 deletions

View File

@ -22,8 +22,10 @@ export class DashboardModel {
editable: any;
graphTooltip: any;
time: any;
originalTime: any;
timepicker: any;
templating: any;
originalTemplating: any;
annotations: any;
refresh: any;
snapshot: any;
@ -68,8 +70,12 @@ 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;
@ -130,7 +136,12 @@ export class DashboardModel {
}
// cleans meta data and other non persistent state
getSaveModelClone() {
getSaveModelClone(options?) {
let defaults = _.defaults(options || {}, {
saveVariables: false,
saveTimerange: false,
});
// make clone
var copy: any = {};
for (var property in this) {
@ -142,10 +153,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) {
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;
}
}
}
if (!defaults.saveTimerange) {
copy.time = this.originalTime;
}
// get panel save models
copy.panels = _.chain(this.panels)
.filter(panel => panel.type !== 'add-panel')

View File

@ -1,4 +1,5 @@
import coreModule from 'app/core/core_module';
import _ from 'lodash';
const template = `
<div class="modal-body">
@ -14,19 +15,29 @@ const template = `
</div>
<form name="ctrl.saveForm" ng-submit="ctrl.save()" class="modal-content" novalidate>
<h6 class="text-center">Add a note to describe your changes</h6>
<div class="p-t-2">
<div class="p-t-1">
<div class="gf-form-group" ng-if="ctrl.timeChange || ctrl.variableChange">
<gf-form-switch class="gf-form"
label="Save current time range" ng-if="ctrl.timeChange" label-class="width-12" switch-class="max-width-6"
checked="ctrl.saveTimerange" on-change="buildUrl()">
</gf-form-switch>
<gf-form-switch class="gf-form"
label="Save current variables" ng-if="ctrl.variableChange" label-class="width-12" switch-class="max-width-6"
checked="ctrl.saveVariables" on-change="buildUrl()">
</gf-form-switch>
</div>
<div class="gf-form">
<label class="gf-form-hint">
<input
type="text"
name="message"
class="gf-form-input"
placeholder="Updates to &hellip;"
placeholder="Add a note to describe your changes &hellip;"
give-focus="true"
ng-model="ctrl.message"
ng-model-options="{allowInvalid: true}"
ng-maxlength="this.max"
maxlength="64"
autocomplete="off" />
<small class="gf-form-hint-text muted" ng-cloak>
<span ng-class="{'text-error': ctrl.saveForm.message.$invalid && ctrl.saveForm.message.$dirty }">
@ -40,7 +51,7 @@ const template = `
<div class="gf-form-button-row text-center">
<button type="submit" class="btn btn-success" ng-disabled="ctrl.saveForm.$invalid">Save</button>
<button class="btn btn-inverse" ng-click="ctrl.dismiss();">Cancel</button>
<a class="btn btn-link" ng-click="ctrl.dismiss();">Cancel</a>
</div>
</form>
</div>
@ -48,14 +59,51 @@ const template = `
export class SaveDashboardModalCtrl {
message: string;
saveVariables = false;
saveTimerange = false;
templating: any;
time: any;
originalTime: any;
current = [];
originalCurrent = [];
max: number;
saveForm: any;
dismiss: () => void;
timeChange = false;
variableChange = false;
/** @ngInject */
constructor(private dashboardSrv) {
this.message = '';
this.max = 64;
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() {
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.variableChange = true);
}
}
return (this.variableChange = false);
} else {
return (this.variableChange = false);
}
}
save() {
@ -63,9 +111,14 @@ export class SaveDashboardModalCtrl {
return;
}
var options = {
saveVariables: this.saveVariables,
saveTimerange: this.saveTimerange,
message: this.message,
};
var dashboard = this.dashboardSrv.getCurrent();
var saveModel = dashboard.getSaveModelClone();
var options = { message: this.message };
var saveModel = dashboard.getSaveModelClone(options);
return this.dashboardSrv.save(saveModel, options).then(this.dismiss);
}

View File

@ -434,4 +434,63 @@ describe('DashboardModel', function() {
});
});
});
describe('save variables and timeline', () => {
let model;
beforeEach(() => {
model = new DashboardModel({
templating: {
list: [
{
name: 'Server',
current: {
selected: true,
text: 'server_001',
value: 'server_001',
},
},
],
},
time: {
from: 'now-6h',
to: 'now',
},
});
model.templating.list[0] = {
name: 'Server',
current: {
selected: true,
text: 'server_002',
value: 'server_002',
},
};
model.time = {
from: 'now-3h',
to: 'now',
};
});
it('should not save variables and timeline', () => {
let options = {
saveVariables: false,
saveTimerange: 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,
};
let saveModel = model.getSaveModelClone(options);
expect(saveModel.templating.list[0].current.text).toBe('server_002');
expect(saveModel.time.from).toBe('now-3h');
});
});
});

View File

@ -0,0 +1,90 @@
import { SaveDashboardModalCtrl } from '../save_modal';
jest.mock('app/core/services/context_srv', () => ({}));
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.variableChange).toBe(true);
});
it('should hide checkboxes', () => {
let fakeDashboardSrv = {
dash: {
templating: {
list: [
{
current: {
selected: true,
//tags: Array(0),
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.variableChange).toBe(false);
});
});
});