diff --git a/public/app/core/utils/sort_by_keys.ts b/public/app/core/utils/sort_by_keys.ts index e7cedc93116..9dff252576a 100644 --- a/public/app/core/utils/sort_by_keys.ts +++ b/public/app/core/utils/sort_by_keys.ts @@ -2,20 +2,14 @@ import _ from 'lodash'; export default function sortByKeys(input) { if (_.isArray(input)) { - var newArray = []; - _.forEach( - input, - function(item) { newArray.push(sortByKeys(item)); } - ); - return newArray; + return input.map(sortByKeys); } if (_.isPlainObject(input)) { var sortedObject = {}; - _.forEach( - _.keys(input).sort(), - function(key) { sortedObject[key] = sortByKeys(input[key]); } - ); + for (let key of _.keys(input).sort()) { + sortedObject[key] = sortByKeys(input[key]); + } return sortedObject; } diff --git a/public/app/features/dashboard/export/exporter.ts b/public/app/features/dashboard/export/exporter.ts index e9f6bd197d4..0a0b4cbdaab 100644 --- a/public/app/features/dashboard/export/exporter.ts +++ b/public/app/features/dashboard/export/exporter.ts @@ -3,7 +3,6 @@ import config from 'app/core/config'; import angular from 'angular'; import _ from 'lodash'; -import sortByKeys from 'app/core/utils/sort_by_keys'; import {DynamicDashboardSrv} from '../dynamic_dashboard_srv'; @@ -152,8 +151,8 @@ export class DashboardExporter { newObj["__requires"] = _.sortBy(requires, ['id']); _.defaults(newObj, saveModel); + return newObj; - return sortByKeys(newObj); }).catch(err => { console.log('Export failed:', err); return { diff --git a/public/app/features/dashboard/model.ts b/public/app/features/dashboard/model.ts index cff82d1ffb0..e31a6c1afd0 100644 --- a/public/app/features/dashboard/model.ts +++ b/public/app/features/dashboard/model.ts @@ -8,6 +8,7 @@ import $ from 'jquery'; import {Emitter, contextSrv, appEvents} from 'app/core/core'; import {DashboardRow} from './row/row_model'; +import sortByKeys from 'app/core/utils/sort_by_keys'; export class DashboardModel { id: any; @@ -36,7 +37,7 @@ export class DashboardModel { events: any; editMode: boolean; - constructor(data, meta) { + constructor(data, meta?) { if (!data) { data = {}; } @@ -107,7 +108,10 @@ export class DashboardModel { this.rows = _.map(rows, row => row.getSaveModel()); this.templating.list = _.map(variables, variable => variable.getSaveModel ? variable.getSaveModel() : variable); + // make clone var copy = $.extend(true, {}, this); + // sort clone + copy = sortByKeys(copy); // restore properties this.events = events; diff --git a/public/app/features/dashboard/specs/dashboard_model_specs.ts b/public/app/features/dashboard/specs/dashboard_model_specs.ts new file mode 100644 index 00000000000..c7d85b8a190 --- /dev/null +++ b/public/app/features/dashboard/specs/dashboard_model_specs.ts @@ -0,0 +1,367 @@ +import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common'; + +import _ from 'lodash'; +import {DashboardModel} from '../model'; + +describe('DashboardModel', function() { + + describe('when creating new dashboard model defaults only', function() { + var model; + + beforeEach(function() { + model = new DashboardModel({}, {}); + }); + + it('should have title', function() { + expect(model.title).to.be('No Title'); + }); + + it('should have meta', function() { + expect(model.meta.canSave).to.be(true); + expect(model.meta.canShare).to.be(true); + }); + + it('should have default properties', function() { + expect(model.rows.length).to.be(0); + }); + }); + + describe('when getting next panel id', function() { + var model; + + beforeEach(function() { + model = new DashboardModel({ + rows: [{ panels: [{ id: 5 }]}] + }); + }); + + it('should return max id + 1', function() { + expect(model.getNextPanelId()).to.be(6); + }); + }); + + describe('getSaveModelClone', function() { + it('should sort keys', () => { + var model = new DashboardModel({}); + var saveModel = model.getSaveModelClone(); + var keys = _.keys(saveModel); + + expect(keys[0]).to.be('addEmptyRow'); + expect(keys[1]).to.be('addPanel'); + }); + }); + + describe('row and panel manipulation', function() { + var dashboard; + + beforeEach(function() { + dashboard = new DashboardModel({}); + }); + + it('adding default should split span in half', function() { + dashboard.addEmptyRow(); + dashboard.rows[0].addPanel({span: 12}); + dashboard.rows[0].addPanel({span: 12}); + + expect(dashboard.rows[0].panels[0].span).to.be(6); + expect(dashboard.rows[0].panels[1].span).to.be(6); + }); + + it('duplicate panel should try to add it to same row', function() { + var panel = { span: 4, attr: '123', id: 10 }; + + dashboard.addEmptyRow(); + dashboard.rows[0].addPanel(panel); + dashboard.duplicatePanel(panel, dashboard.rows[0]); + + expect(dashboard.rows[0].panels[0].span).to.be(4); + expect(dashboard.rows[0].panels[1].span).to.be(4); + expect(dashboard.rows[0].panels[1].attr).to.be('123'); + expect(dashboard.rows[0].panels[1].id).to.be(11); + }); + + it('duplicate panel should remove repeat data', function() { + var panel = { span: 4, attr: '123', id: 10, repeat: 'asd', scopedVars: { test: 'asd' }}; + + dashboard.addEmptyRow(); + dashboard.rows[0].addPanel(panel); + dashboard.duplicatePanel(panel, dashboard.rows[0]); + + expect(dashboard.rows[0].panels[1].repeat).to.be(undefined); + expect(dashboard.rows[0].panels[1].scopedVars).to.be(undefined); + }); + + }); + + describe('when creating dashboard with old schema', function() { + var model; + var graph; + var singlestat; + var table; + + beforeEach(function() { + model = new DashboardModel({ + services: { filter: { time: { from: 'now-1d', to: 'now'}, list: [{}] }}, + pulldowns: [ + {type: 'filtering', enable: true}, + {type: 'annotations', enable: true, annotations: [{name: 'old'}]} + ], + rows: [ + { + panels: [ + { + type: 'graph', legend: true, aliasYAxis: { test: 2 }, + y_formats: ['kbyte', 'ms'], + grid: { + min: 1, + max: 10, + rightMin: 5, + rightMax: 15, + leftLogBase: 1, + rightLogBase: 2, + threshold1: 200, + threshold2: 400, + threshold1Color: 'yellow', + threshold2Color: 'red', + }, + leftYAxisLabel: 'left label', + targets: [{refId: 'A'}, {}], + }, + { + type: 'singlestat', legend: true, thresholds: '10,20,30', aliasYAxis: { test: 2 }, grid: { min: 1, max: 10 }, + targets: [{refId: 'A'}, {}], + }, + { + type: 'table', legend: true, styles: [{ thresholds: ["10", "20", "30"]}, { thresholds: ["100", "200", "300"]}], + targets: [{refId: 'A'}, {}], + } + ] + } + ] + }); + + graph = model.rows[0].panels[0]; + singlestat = model.rows[0].panels[1]; + table = model.rows[0].panels[2]; + }); + + it('should have title', function() { + expect(model.title).to.be('No Title'); + }); + + it('should have panel id', function() { + expect(graph.id).to.be(1); + }); + + it('should move time and filtering list', function() { + expect(model.time.from).to.be('now-1d'); + expect(model.templating.list[0].allFormat).to.be('glob'); + }); + + it('graphite panel should change name too graph', function() { + expect(graph.type).to.be('graph'); + }); + + it('single stat panel should have two thresholds', function() { + expect(singlestat.thresholds).to.be('20,30'); + }); + + it('queries without refId should get it', function() { + expect(graph.targets[1].refId).to.be('B'); + }); + + it('update legend setting', function() { + expect(graph.legend.show).to.be(true); + }); + + it('move aliasYAxis to series override', function() { + expect(graph.seriesOverrides[0].alias).to.be("test"); + expect(graph.seriesOverrides[0].yaxis).to.be(2); + }); + + it('should move pulldowns to new schema', function() { + expect(model.annotations.list[0].name).to.be('old'); + }); + + it('table panel should only have two thresholds values', function() { + expect(table.styles[0].thresholds[0]).to.be("20"); + expect(table.styles[0].thresholds[1]).to.be("30"); + expect(table.styles[1].thresholds[0]).to.be("200"); + expect(table.styles[1].thresholds[1]).to.be("300"); + }); + + it('graph grid to yaxes options', function() { + expect(graph.yaxes[0].min).to.be(1); + expect(graph.yaxes[0].max).to.be(10); + expect(graph.yaxes[0].format).to.be('kbyte'); + expect(graph.yaxes[0].label).to.be('left label'); + expect(graph.yaxes[0].logBase).to.be(1); + expect(graph.yaxes[1].min).to.be(5); + expect(graph.yaxes[1].max).to.be(15); + expect(graph.yaxes[1].format).to.be('ms'); + expect(graph.yaxes[1].logBase).to.be(2); + + expect(graph.grid.rightMax).to.be(undefined); + expect(graph.grid.rightLogBase).to.be(undefined); + expect(graph.y_formats).to.be(undefined); + }); + + it('dashboard schema version should be set to latest', function() { + expect(model.schemaVersion).to.be(14); + }); + + it('graph thresholds should be migrated', function() { + expect(graph.thresholds.length).to.be(2); + expect(graph.thresholds[0].op).to.be('gt'); + expect(graph.thresholds[0].value).to.be(200); + expect(graph.thresholds[0].fillColor).to.be('yellow'); + expect(graph.thresholds[1].value).to.be(400); + expect(graph.thresholds[1].fillColor).to.be('red'); + }); + }); + + describe('when creating dashboard model with missing list for annoations or templating', function() { + var model; + + beforeEach(function() { + model = new DashboardModel({ + annotations: { + enable: true, + }, + templating: { + enable: true + } + }); + }); + + it('should add empty list', function() { + expect(model.annotations.list.length).to.be(0); + expect(model.templating.list.length).to.be(0); + }); + }); + + describe('Given editable false dashboard', function() { + var model; + + beforeEach(function() { + model = new DashboardModel({editable: false}); + }); + + it('Should set meta canEdit and canSave to false', function() { + expect(model.meta.canSave).to.be(false); + expect(model.meta.canEdit).to.be(false); + }); + + it('getSaveModelClone should remove meta', function() { + var clone = model.getSaveModelClone(); + expect(clone.meta).to.be(undefined); + }); + }); + + describe('when loading dashboard with old influxdb query schema', function() { + var model; + var target; + + beforeEach(function() { + model = new DashboardModel({ + rows: [{ + panels: [{ + type: 'graph', + grid: {}, + yaxes: [{}, {}], + targets: [{ + "alias": "$tag_datacenter $tag_source $col", + "column": "value", + "measurement": "logins.count", + "fields": [ + { + "func": "mean", + "name": "value", + "mathExpr": "*2", + "asExpr": "value" + }, + { + "name": "one-minute", + "func": "mean", + "mathExpr": "*3", + "asExpr": "one-minute" + } + ], + "tags": [], + "fill": "previous", + "function": "mean", + "groupBy": [ + { + "interval": "auto", + "type": "time" + }, + { + "key": "source", + "type": "tag" + }, + { + "type": "tag", + "key": "datacenter" + } + ], + }] + }] + }] + }); + + target = model.rows[0].panels[0].targets[0]; + }); + + it('should update query schema', function() { + expect(target.fields).to.be(undefined); + expect(target.select.length).to.be(2); + expect(target.select[0].length).to.be(4); + expect(target.select[0][0].type).to.be('field'); + expect(target.select[0][1].type).to.be('mean'); + expect(target.select[0][2].type).to.be('math'); + expect(target.select[0][3].type).to.be('alias'); + }); + + }); + + describe('when creating dashboard model with missing list for annoations or templating', function() { + var model; + + beforeEach(function() { + model = new DashboardModel({ + annotations: { + enable: true, + }, + templating: { + enable: true + } + }); + }); + + it('should add empty list', function() { + expect(model.annotations.list.length).to.be(0); + expect(model.templating.list.length).to.be(0); + }); + }); + + describe('Formatting epoch timestamp when timezone is set as utc', function() { + var dashboard; + + beforeEach(function() { + dashboard = new DashboardModel({timezone: 'utc'}); + }); + + it('Should format timestamp with second resolution by default', function() { + expect(dashboard.formatDate(1234567890000)).to.be('2009-02-13 23:31:30'); + }); + + it('Should format timestamp with second resolution even if second format is passed as parameter', function() { + expect(dashboard.formatDate(1234567890007,'YYYY-MM-DD HH:mm:ss')).to.be('2009-02-13 23:31:30'); + }); + + it('Should format timestamp with millisecond resolution if format is passed as parameter', function() { + expect(dashboard.formatDate(1234567890007,'YYYY-MM-DD HH:mm:ss.SSS')).to.be('2009-02-13 23:31:30.007'); + }); + }); + +}); diff --git a/public/app/features/dashboard/specs/dashboard_srv_specs.ts b/public/app/features/dashboard/specs/dashboard_srv_specs.ts index 974b8600c0c..666a3bc0bf2 100644 --- a/public/app/features/dashboard/specs/dashboard_srv_specs.ts +++ b/public/app/features/dashboard/specs/dashboard_srv_specs.ts @@ -9,370 +9,4 @@ describe('dashboardSrv', function() { _dashboardSrv = new DashboardSrv({}, {}, {}); }); - describe('when creating new dashboard with defaults only', function() { - var model; - - beforeEach(function() { - model = _dashboardSrv.create({}, {}); - }); - - it('should have title', function() { - expect(model.title).to.be('No Title'); - }); - - it('should have meta', function() { - expect(model.meta.canSave).to.be(true); - expect(model.meta.canShare).to.be(true); - }); - - it('should have default properties', function() { - expect(model.rows.length).to.be(0); - }); - }); - - describe('when getting next panel id', function() { - var model; - - beforeEach(function() { - model = _dashboardSrv.create({ - rows: [{ panels: [{ id: 5 }]}] - }); - }); - - it('should return max id + 1', function() { - expect(model.getNextPanelId()).to.be(6); - }); - }); - - describe('row and panel manipulation', function() { - var dashboard; - - beforeEach(function() { - dashboard = _dashboardSrv.create({}); - }); - - it('adding default should split span in half', function() { - dashboard.addEmptyRow(); - dashboard.rows[0].addPanel({span: 12}); - dashboard.rows[0].addPanel({span: 12}); - - expect(dashboard.rows[0].panels[0].span).to.be(6); - expect(dashboard.rows[0].panels[1].span).to.be(6); - }); - - it('duplicate panel should try to add it to same row', function() { - var panel = { span: 4, attr: '123', id: 10 }; - - dashboard.addEmptyRow(); - dashboard.rows[0].addPanel(panel); - dashboard.duplicatePanel(panel, dashboard.rows[0]); - - expect(dashboard.rows[0].panels[0].span).to.be(4); - expect(dashboard.rows[0].panels[1].span).to.be(4); - expect(dashboard.rows[0].panels[1].attr).to.be('123'); - expect(dashboard.rows[0].panels[1].id).to.be(11); - }); - - it('duplicate panel should remove repeat data', function() { - var panel = { span: 4, attr: '123', id: 10, repeat: 'asd', scopedVars: { test: 'asd' }}; - - dashboard.addEmptyRow(); - dashboard.rows[0].addPanel(panel); - dashboard.duplicatePanel(panel, dashboard.rows[0]); - - expect(dashboard.rows[0].panels[1].repeat).to.be(undefined); - expect(dashboard.rows[0].panels[1].scopedVars).to.be(undefined); - }); - - }); - - describe('when creating dashboard with editable false', function() { - var model; - - beforeEach(function() { - model = _dashboardSrv.create({ - editable: false - }); - }); - - it('should set editable false', function() { - expect(model.editable).to.be(false); - }); - - }); - - describe('when creating dashboard with old schema', function() { - var model; - var graph; - var singlestat; - var table; - - beforeEach(function() { - model = _dashboardSrv.create({ - services: { filter: { time: { from: 'now-1d', to: 'now'}, list: [{}] }}, - pulldowns: [ - {type: 'filtering', enable: true}, - {type: 'annotations', enable: true, annotations: [{name: 'old'}]} - ], - rows: [ - { - panels: [ - { - type: 'graph', legend: true, aliasYAxis: { test: 2 }, - y_formats: ['kbyte', 'ms'], - grid: { - min: 1, - max: 10, - rightMin: 5, - rightMax: 15, - leftLogBase: 1, - rightLogBase: 2, - threshold1: 200, - threshold2: 400, - threshold1Color: 'yellow', - threshold2Color: 'red', - }, - leftYAxisLabel: 'left label', - targets: [{refId: 'A'}, {}], - }, - { - type: 'singlestat', legend: true, thresholds: '10,20,30', aliasYAxis: { test: 2 }, grid: { min: 1, max: 10 }, - targets: [{refId: 'A'}, {}], - }, - { - type: 'table', legend: true, styles: [{ thresholds: ["10", "20", "30"]}, { thresholds: ["100", "200", "300"]}], - targets: [{refId: 'A'}, {}], - } - ] - } - ] - }); - - graph = model.rows[0].panels[0]; - singlestat = model.rows[0].panels[1]; - table = model.rows[0].panels[2]; - }); - - it('should have title', function() { - expect(model.title).to.be('No Title'); - }); - - it('should have panel id', function() { - expect(graph.id).to.be(1); - }); - - it('should move time and filtering list', function() { - expect(model.time.from).to.be('now-1d'); - expect(model.templating.list[0].allFormat).to.be('glob'); - }); - - it('graphite panel should change name too graph', function() { - expect(graph.type).to.be('graph'); - }); - - it('single stat panel should have two thresholds', function() { - expect(singlestat.thresholds).to.be('20,30'); - }); - - it('queries without refId should get it', function() { - expect(graph.targets[1].refId).to.be('B'); - }); - - it('update legend setting', function() { - expect(graph.legend.show).to.be(true); - }); - - it('move aliasYAxis to series override', function() { - expect(graph.seriesOverrides[0].alias).to.be("test"); - expect(graph.seriesOverrides[0].yaxis).to.be(2); - }); - - it('should move pulldowns to new schema', function() { - expect(model.annotations.list[0].name).to.be('old'); - }); - - it('table panel should only have two thresholds values', function() { - expect(table.styles[0].thresholds[0]).to.be("20"); - expect(table.styles[0].thresholds[1]).to.be("30"); - expect(table.styles[1].thresholds[0]).to.be("200"); - expect(table.styles[1].thresholds[1]).to.be("300"); - }); - - it('graph grid to yaxes options', function() { - expect(graph.yaxes[0].min).to.be(1); - expect(graph.yaxes[0].max).to.be(10); - expect(graph.yaxes[0].format).to.be('kbyte'); - expect(graph.yaxes[0].label).to.be('left label'); - expect(graph.yaxes[0].logBase).to.be(1); - expect(graph.yaxes[1].min).to.be(5); - expect(graph.yaxes[1].max).to.be(15); - expect(graph.yaxes[1].format).to.be('ms'); - expect(graph.yaxes[1].logBase).to.be(2); - - expect(graph.grid.rightMax).to.be(undefined); - expect(graph.grid.rightLogBase).to.be(undefined); - expect(graph.y_formats).to.be(undefined); - }); - - it('dashboard schema version should be set to latest', function() { - expect(model.schemaVersion).to.be(14); - }); - - it('graph thresholds should be migrated', function() { - expect(graph.thresholds.length).to.be(2); - expect(graph.thresholds[0].op).to.be('gt'); - expect(graph.thresholds[0].value).to.be(200); - expect(graph.thresholds[0].fillColor).to.be('yellow'); - expect(graph.thresholds[1].value).to.be(400); - expect(graph.thresholds[1].fillColor).to.be('red'); - }); - }); - - describe('when creating dashboard model with missing list for annoations or templating', function() { - var model; - - beforeEach(function() { - model = _dashboardSrv.create({ - annotations: { - enable: true, - }, - templating: { - enable: true - } - }); - }); - - it('should add empty list', function() { - expect(model.annotations.list.length).to.be(0); - expect(model.templating.list.length).to.be(0); - }); - }); - - describe('Given editable false dashboard', function() { - var model; - - beforeEach(function() { - model = _dashboardSrv.create({ - editable: false, - }); - }); - - it('Should set meta canEdit and canSave to false', function() { - expect(model.meta.canSave).to.be(false); - expect(model.meta.canEdit).to.be(false); - }); - - it('getSaveModelClone should remove meta', function() { - var clone = model.getSaveModelClone(); - expect(clone.meta).to.be(undefined); - }); - }); - - describe('when loading dashboard with old influxdb query schema', function() { - var model; - var target; - - beforeEach(function() { - model = _dashboardSrv.create({ - rows: [{ - panels: [{ - type: 'graph', - grid: {}, - yaxes: [{}, {}], - targets: [{ - "alias": "$tag_datacenter $tag_source $col", - "column": "value", - "measurement": "logins.count", - "fields": [ - { - "func": "mean", - "name": "value", - "mathExpr": "*2", - "asExpr": "value" - }, - { - "name": "one-minute", - "func": "mean", - "mathExpr": "*3", - "asExpr": "one-minute" - } - ], - "tags": [], - "fill": "previous", - "function": "mean", - "groupBy": [ - { - "interval": "auto", - "type": "time" - }, - { - "key": "source", - "type": "tag" - }, - { - "type": "tag", - "key": "datacenter" - } - ], - }] - }] - }] - }); - - target = model.rows[0].panels[0].targets[0]; - }); - - it('should update query schema', function() { - expect(target.fields).to.be(undefined); - expect(target.select.length).to.be(2); - expect(target.select[0].length).to.be(4); - expect(target.select[0][0].type).to.be('field'); - expect(target.select[0][1].type).to.be('mean'); - expect(target.select[0][2].type).to.be('math'); - expect(target.select[0][3].type).to.be('alias'); - }); - - }); - - describe('when creating dashboard model with missing list for annoations or templating', function() { - var model; - - beforeEach(function() { - model = _dashboardSrv.create({ - annotations: { - enable: true, - }, - templating: { - enable: true - } - }); - }); - - it('should add empty list', function() { - expect(model.annotations.list.length).to.be(0); - expect(model.templating.list.length).to.be(0); - }); - }); - - describe('Formatting epoch timestamp when timezone is set as utc', function() { - var dashboard; - - beforeEach(function() { - dashboard = _dashboardSrv.create({ - timezone: 'utc', - }); - }); - - it('Should format timestamp with second resolution by default', function() { - expect(dashboard.formatDate(1234567890000)).to.be('2009-02-13 23:31:30'); - }); - - it('Should format timestamp with second resolution even if second format is passed as parameter', function() { - expect(dashboard.formatDate(1234567890007,'YYYY-MM-DD HH:mm:ss')).to.be('2009-02-13 23:31:30'); - }); - - it('Should format timestamp with millisecond resolution if format is passed as parameter', function() { - expect(dashboard.formatDate(1234567890007,'YYYY-MM-DD HH:mm:ss.SSS')).to.be('2009-02-13 23:31:30.007'); - }); - }); });