diff --git a/public/app/core/constants.ts b/public/app/core/constants.ts index 5a0a8521758..6e55ce030ed 100644 --- a/public/app/core/constants.ts +++ b/public/app/core/constants.ts @@ -4,4 +4,6 @@ export const GRID_CELL_VMARGIN = 10; export const GRID_COLUMN_COUNT = 24; export const REPEAT_DIR_VERTICAL = 'v'; - +export const DEFAULT_PANEL_SPAN = 4; +export const DEFAULT_ROW_HEIGHT = 250; +export const MIN_PANEL_HEIGHT = GRID_CELL_HEIGHT * 3; diff --git a/public/app/features/dashboard/dashboard_migration.ts b/public/app/features/dashboard/dashboard_migration.ts new file mode 100644 index 00000000000..d615f4934a8 --- /dev/null +++ b/public/app/features/dashboard/dashboard_migration.ts @@ -0,0 +1,532 @@ +import _ from 'lodash'; +import {GRID_COLUMN_COUNT, GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, + DEFAULT_ROW_HEIGHT, MIN_PANEL_HEIGHT, DEFAULT_PANEL_SPAN} from 'app/core/constants'; +import {PanelModel} from './panel_model'; +import {DashboardModel} from './dashboard_model'; + +export class DashboardMigrator { + dashboard: DashboardModel; + + constructor(dashboardModel: DashboardModel) { + this.dashboard = dashboardModel; + } + + updateSchema(old) { + var i, j, k; + var oldVersion = this.dashboard.schemaVersion; + var panelUpgrades = []; + this.dashboard.schemaVersion = 16; + + if (oldVersion === this.dashboard.schemaVersion) { + return; + } + + // version 2 schema changes + if (oldVersion < 2) { + if (old.services) { + if (old.services.filter) { + this.dashboard.time = old.services.filter.time; + this.dashboard.templating.list = old.services.filter.list || []; + } + } + + panelUpgrades.push(function(panel) { + // rename panel type + if (panel.type === 'graphite') { + panel.type = 'graph'; + } + + if (panel.type !== 'graph') { + return; + } + + if (_.isBoolean(panel.legend)) { + panel.legend = {show: panel.legend}; + } + + if (panel.grid) { + if (panel.grid.min) { + panel.grid.leftMin = panel.grid.min; + delete panel.grid.min; + } + + if (panel.grid.max) { + panel.grid.leftMax = panel.grid.max; + delete panel.grid.max; + } + } + + if (panel.y_format) { + panel.y_formats[0] = panel.y_format; + delete panel.y_format; + } + + if (panel.y2_format) { + panel.y_formats[1] = panel.y2_format; + delete panel.y2_format; + } + }); + } + + // schema version 3 changes + if (oldVersion < 3) { + // ensure panel ids + var maxId = this.dashboard.getNextPanelId(); + panelUpgrades.push(function(panel) { + if (!panel.id) { + panel.id = maxId; + maxId += 1; + } + }); + } + + // schema version 4 changes + if (oldVersion < 4) { + // move aliasYAxis changes + panelUpgrades.push(function(panel) { + if (panel.type !== 'graph') { + return; + } + _.each(panel.aliasYAxis, function(value, key) { + panel.seriesOverrides = [{alias: key, yaxis: value}]; + }); + delete panel.aliasYAxis; + }); + } + + if (oldVersion < 6) { + // move pulldowns to new schema + var annotations = _.find(old.pulldowns, {type: 'annotations'}); + + if (annotations) { + this.dashboard.annotations = { + list: annotations.annotations || [], + }; + } + + // update template variables + for (i = 0; i < this.dashboard.templating.list.length; i++) { + var variable = this.dashboard.templating.list[i]; + if (variable.datasource === void 0) { + variable.datasource = null; + } + if (variable.type === 'filter') { + variable.type = 'query'; + } + if (variable.type === void 0) { + variable.type = 'query'; + } + if (variable.allFormat === void 0) { + variable.allFormat = 'glob'; + } + } + } + + if (oldVersion < 7) { + if (old.nav && old.nav.length) { + this.dashboard.timepicker = old.nav[0]; + } + + // ensure query refIds + panelUpgrades.push(function(panel) { + _.each( + panel.targets, + function(target) { + if (!target.refId) { + target.refId = this.dashboard.getNextQueryLetter(panel); + } + }.bind(this), + ); + }); + } + + if (oldVersion < 8) { + panelUpgrades.push(function(panel) { + _.each(panel.targets, function(target) { + // update old influxdb query schema + if (target.fields && target.tags && target.groupBy) { + if (target.rawQuery) { + delete target.fields; + delete target.fill; + } else { + target.select = _.map(target.fields, function(field) { + var parts = []; + parts.push({type: 'field', params: [field.name]}); + parts.push({type: field.func, params: []}); + if (field.mathExpr) { + parts.push({type: 'math', params: [field.mathExpr]}); + } + if (field.asExpr) { + parts.push({type: 'alias', params: [field.asExpr]}); + } + return parts; + }); + delete target.fields; + _.each(target.groupBy, function(part) { + if (part.type === 'time' && part.interval) { + part.params = [part.interval]; + delete part.interval; + } + if (part.type === 'tag' && part.key) { + part.params = [part.key]; + delete part.key; + } + }); + + if (target.fill) { + target.groupBy.push({type: 'fill', params: [target.fill]}); + delete target.fill; + } + } + } + }); + }); + } + + // schema version 9 changes + if (oldVersion < 9) { + // move aliasYAxis changes + panelUpgrades.push(function(panel) { + if (panel.type !== 'singlestat' && panel.thresholds !== '') { + return; + } + + if (panel.thresholds) { + var k = panel.thresholds.split(','); + + if (k.length >= 3) { + k.shift(); + panel.thresholds = k.join(','); + } + } + }); + } + + // schema version 10 changes + if (oldVersion < 10) { + // move aliasYAxis changes + panelUpgrades.push(function(panel) { + if (panel.type !== 'table') { + return; + } + + _.each(panel.styles, function(style) { + if (style.thresholds && style.thresholds.length >= 3) { + var k = style.thresholds; + k.shift(); + style.thresholds = k; + } + }); + }); + } + + if (oldVersion < 12) { + // update template variables + _.each(this.dashboard.templating.list, function(templateVariable) { + if (templateVariable.refresh) { + templateVariable.refresh = 1; + } + if (!templateVariable.refresh) { + templateVariable.refresh = 0; + } + if (templateVariable.hideVariable) { + templateVariable.hide = 2; + } else if (templateVariable.hideLabel) { + templateVariable.hide = 1; + } + }); + } + + if (oldVersion < 12) { + // update graph yaxes changes + panelUpgrades.push(function(panel) { + if (panel.type !== 'graph') { + return; + } + if (!panel.grid) { + return; + } + + if (!panel.yaxes) { + panel.yaxes = [ + { + show: panel['y-axis'], + min: panel.grid.leftMin, + max: panel.grid.leftMax, + logBase: panel.grid.leftLogBase, + format: panel.y_formats[0], + label: panel.leftYAxisLabel, + }, + { + show: panel['y-axis'], + min: panel.grid.rightMin, + max: panel.grid.rightMax, + logBase: panel.grid.rightLogBase, + format: panel.y_formats[1], + label: panel.rightYAxisLabel, + }, + ]; + + panel.xaxis = { + show: panel['x-axis'], + }; + + delete panel.grid.leftMin; + delete panel.grid.leftMax; + delete panel.grid.leftLogBase; + delete panel.grid.rightMin; + delete panel.grid.rightMax; + delete panel.grid.rightLogBase; + delete panel.y_formats; + delete panel.leftYAxisLabel; + delete panel.rightYAxisLabel; + delete panel['y-axis']; + delete panel['x-axis']; + } + }); + } + + if (oldVersion < 13) { + // update graph yaxes changes + panelUpgrades.push(function(panel) { + if (panel.type !== 'graph') { + return; + } + if (!panel.grid) { + return; + } + + panel.thresholds = []; + var t1: any = {}, + t2: any = {}; + + if (panel.grid.threshold1 !== null) { + t1.value = panel.grid.threshold1; + if (panel.grid.thresholdLine) { + t1.line = true; + t1.lineColor = panel.grid.threshold1Color; + t1.colorMode = 'custom'; + } else { + t1.fill = true; + t1.fillColor = panel.grid.threshold1Color; + t1.colorMode = 'custom'; + } + } + + if (panel.grid.threshold2 !== null) { + t2.value = panel.grid.threshold2; + if (panel.grid.thresholdLine) { + t2.line = true; + t2.lineColor = panel.grid.threshold2Color; + t2.colorMode = 'custom'; + } else { + t2.fill = true; + t2.fillColor = panel.grid.threshold2Color; + t2.colorMode = 'custom'; + } + } + + if (_.isNumber(t1.value)) { + if (_.isNumber(t2.value)) { + if (t1.value > t2.value) { + t1.op = t2.op = 'lt'; + panel.thresholds.push(t1); + panel.thresholds.push(t2); + } else { + t1.op = t2.op = 'gt'; + panel.thresholds.push(t1); + panel.thresholds.push(t2); + } + } else { + t1.op = 'gt'; + panel.thresholds.push(t1); + } + } + + delete panel.grid.threshold1; + delete panel.grid.threshold1Color; + delete panel.grid.threshold2; + delete panel.grid.threshold2Color; + delete panel.grid.thresholdLine; + }); + } + + if (oldVersion < 14) { + this.dashboard.graphTooltip = old.sharedCrosshair ? 1 : 0; + } + + if (oldVersion < 16) { + this.upgradeToGridLayout(old); + } + + if (panelUpgrades.length === 0) { + return; + } + + for (j = 0; j < this.dashboard.panels.length; j++) { + for (k = 0; k < panelUpgrades.length; k++) { + panelUpgrades[k].call(this, this.dashboard.panels[j]); + } + } + } + + upgradeToGridLayout(old) { + let yPos = 0; + let widthFactor = GRID_COLUMN_COUNT / 12; + + const maxPanelId = _.max(_.flattenDeep(_.map(old.rows, (row) => { + return _.map(row.panels, 'id'); + }))); + let nextRowId = maxPanelId + 1; + + if (!old.rows) { + return; + } + + // Add special "row" panels if even one row is collapsed or has visible title + const showRows = _.some(old.rows, (row) => row.collapse || row.showTitle); + + for (let row of old.rows) { + let xPos = 0; + let height: any = row.height || DEFAULT_ROW_HEIGHT; + const rowGridHeight = getGridHeight(height); + + let rowPanel: any = {}; + let rowPanelModel: PanelModel; + if (showRows) { + // add special row panel + rowPanel.id = nextRowId; + rowPanel.type = 'row'; + rowPanel.title = row.title; + rowPanel.collapsed = row.collapse; + rowPanel.panels = []; + rowPanel.gridPos = {x: 0, y: yPos, w: GRID_COLUMN_COUNT, h: rowGridHeight}; + rowPanelModel = new PanelModel(rowPanel); + nextRowId++; + yPos++; + } + + let rowArea = new RowArea(rowGridHeight, GRID_COLUMN_COUNT, yPos); + + for (let panel of row.panels) { + panel.span = panel.span || DEFAULT_PANEL_SPAN; + const panelWidth = Math.floor(panel.span) * widthFactor; + const panelHeight = panel.height ? getGridHeight(panel.height) : rowGridHeight; + + let panelPos = rowArea.getPanelPosition(panelHeight, panelWidth); + yPos = rowArea.yPos; + panel.gridPos = {x: panelPos.x, y: yPos + panelPos.y, w: panelWidth, h: panelHeight}; + rowArea.addPanel(panel.gridPos); + + delete panel.span; + + xPos += panel.gridPos.w; + + if (rowPanelModel && rowPanel.collapsed) { + rowPanelModel.panels.push(panel); + } else { + this.dashboard.panels.push(new PanelModel(panel)); + } + } + + if (rowPanelModel) { + this.dashboard.panels.push(rowPanelModel); + } + + if (!(rowPanelModel && rowPanel.collapsed)) { + yPos += rowGridHeight; + } + } + } +} + +function getGridHeight(height) { + if (_.isString(height)) { + height = parseInt(height.replace('px', ''), 10); + } + + if (height < MIN_PANEL_HEIGHT) { + height = MIN_PANEL_HEIGHT; + } + + const gridHeight = Math.ceil(height / (GRID_CELL_HEIGHT + GRID_CELL_VMARGIN)); + return gridHeight; +} + +/** + * RowArea represents dashboard row filled by panels + * area is an array of numbers represented filled column's cells like + * ----------------------- + * |******** **** + * |******** **** + * |******** + * ----------------------- + * 33333333 2222 00000 ... + */ +class RowArea { + area: number[]; + yPos: number; + height: number; + + constructor(height, width = GRID_COLUMN_COUNT, rowYPos = 0) { + this.area = new Array(width).fill(0); + this.yPos = rowYPos; + this.height = height; + } + + reset() { + this.area.fill(0); + } + + /** + * Update area after adding the panel. + */ + addPanel(gridPos) { + for (let i = gridPos.x; i < gridPos.x + gridPos.w; i++) { + if (!this.area[i] || gridPos.y + gridPos.h - this.yPos > this.area[i]) { + this.area[i] = gridPos.y + gridPos.h - this.yPos; + } + } + return this.area; + } + + /** + * Calculate position for the new panel in the row. + */ + getPanelPosition(panelHeight, panelWidth, callOnce = false) { + let startPlace, endPlace; + let place; + for (let i = this.area.length - 1; i >= 0; i--) { + if (this.height - this.area[i] > 0) { + if (endPlace === undefined) { + endPlace = i; + } else { + if (i < this.area.length - 1 && this.area[i] <= this.area[i+1]) { + startPlace = i; + } else { + break; + } + } + } else { + break; + } + } + + if (startPlace !== undefined && endPlace !== undefined && endPlace - startPlace >= panelWidth - 1) { + const yPos = _.max(this.area.slice(startPlace)); + place = { + x: startPlace, + y: yPos + }; + } else if (!callOnce) { + // wrap to next row + this.yPos += this.height; + this.reset(); + return this.getPanelPosition(panelHeight, panelWidth, true); + } else { + return null; + } + + return place; + } +} diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts index 9779302eb42..0e37acb201a 100644 --- a/public/app/features/dashboard/dashboard_model.ts +++ b/public/app/features/dashboard/dashboard_model.ts @@ -1,13 +1,14 @@ import moment from 'moment'; import _ from 'lodash'; -import {GRID_COLUMN_COUNT, GRID_CELL_HEIGHT, REPEAT_DIR_VERTICAL} from 'app/core/constants'; +import {GRID_COLUMN_COUNT, REPEAT_DIR_VERTICAL} from 'app/core/constants'; import {DEFAULT_ANNOTATION_COLOR} from 'app/core/utils/colors'; import {Emitter} from 'app/core/utils/emitter'; import {contextSrv} from 'app/core/services/context_srv'; import sortByKeys from 'app/core/utils/sort_by_keys'; import {PanelModel} from './panel_model'; +import {DashboardMigrator} from './dashboard_migration'; export class DashboardModel { id: any; @@ -554,416 +555,7 @@ export class DashboardModel { } private updateSchema(old) { - var i, j, k; - var oldVersion = this.schemaVersion; - var panelUpgrades = []; - this.schemaVersion = 16; - - if (oldVersion === this.schemaVersion) { - return; - } - - // version 2 schema changes - if (oldVersion < 2) { - if (old.services) { - if (old.services.filter) { - this.time = old.services.filter.time; - this.templating.list = old.services.filter.list || []; - } - } - - panelUpgrades.push(function(panel) { - // rename panel type - if (panel.type === 'graphite') { - panel.type = 'graph'; - } - - if (panel.type !== 'graph') { - return; - } - - if (_.isBoolean(panel.legend)) { - panel.legend = {show: panel.legend}; - } - - if (panel.grid) { - if (panel.grid.min) { - panel.grid.leftMin = panel.grid.min; - delete panel.grid.min; - } - - if (panel.grid.max) { - panel.grid.leftMax = panel.grid.max; - delete panel.grid.max; - } - } - - if (panel.y_format) { - panel.y_formats[0] = panel.y_format; - delete panel.y_format; - } - - if (panel.y2_format) { - panel.y_formats[1] = panel.y2_format; - delete panel.y2_format; - } - }); - } - - // schema version 3 changes - if (oldVersion < 3) { - // ensure panel ids - var maxId = this.getNextPanelId(); - panelUpgrades.push(function(panel) { - if (!panel.id) { - panel.id = maxId; - maxId += 1; - } - }); - } - - // schema version 4 changes - if (oldVersion < 4) { - // move aliasYAxis changes - panelUpgrades.push(function(panel) { - if (panel.type !== 'graph') { - return; - } - _.each(panel.aliasYAxis, function(value, key) { - panel.seriesOverrides = [{alias: key, yaxis: value}]; - }); - delete panel.aliasYAxis; - }); - } - - if (oldVersion < 6) { - // move pulldowns to new schema - var annotations = _.find(old.pulldowns, {type: 'annotations'}); - - if (annotations) { - this.annotations = { - list: annotations.annotations || [], - }; - } - - // update template variables - for (i = 0; i < this.templating.list.length; i++) { - var variable = this.templating.list[i]; - if (variable.datasource === void 0) { - variable.datasource = null; - } - if (variable.type === 'filter') { - variable.type = 'query'; - } - if (variable.type === void 0) { - variable.type = 'query'; - } - if (variable.allFormat === void 0) { - variable.allFormat = 'glob'; - } - } - } - - if (oldVersion < 7) { - if (old.nav && old.nav.length) { - this.timepicker = old.nav[0]; - } - - // ensure query refIds - panelUpgrades.push(function(panel) { - _.each( - panel.targets, - function(target) { - if (!target.refId) { - target.refId = this.getNextQueryLetter(panel); - } - }.bind(this), - ); - }); - } - - if (oldVersion < 8) { - panelUpgrades.push(function(panel) { - _.each(panel.targets, function(target) { - // update old influxdb query schema - if (target.fields && target.tags && target.groupBy) { - if (target.rawQuery) { - delete target.fields; - delete target.fill; - } else { - target.select = _.map(target.fields, function(field) { - var parts = []; - parts.push({type: 'field', params: [field.name]}); - parts.push({type: field.func, params: []}); - if (field.mathExpr) { - parts.push({type: 'math', params: [field.mathExpr]}); - } - if (field.asExpr) { - parts.push({type: 'alias', params: [field.asExpr]}); - } - return parts; - }); - delete target.fields; - _.each(target.groupBy, function(part) { - if (part.type === 'time' && part.interval) { - part.params = [part.interval]; - delete part.interval; - } - if (part.type === 'tag' && part.key) { - part.params = [part.key]; - delete part.key; - } - }); - - if (target.fill) { - target.groupBy.push({type: 'fill', params: [target.fill]}); - delete target.fill; - } - } - } - }); - }); - } - - // schema version 9 changes - if (oldVersion < 9) { - // move aliasYAxis changes - panelUpgrades.push(function(panel) { - if (panel.type !== 'singlestat' && panel.thresholds !== '') { - return; - } - - if (panel.thresholds) { - var k = panel.thresholds.split(','); - - if (k.length >= 3) { - k.shift(); - panel.thresholds = k.join(','); - } - } - }); - } - - // schema version 10 changes - if (oldVersion < 10) { - // move aliasYAxis changes - panelUpgrades.push(function(panel) { - if (panel.type !== 'table') { - return; - } - - _.each(panel.styles, function(style) { - if (style.thresholds && style.thresholds.length >= 3) { - var k = style.thresholds; - k.shift(); - style.thresholds = k; - } - }); - }); - } - - if (oldVersion < 12) { - // update template variables - _.each(this.templating.list, function(templateVariable) { - if (templateVariable.refresh) { - templateVariable.refresh = 1; - } - if (!templateVariable.refresh) { - templateVariable.refresh = 0; - } - if (templateVariable.hideVariable) { - templateVariable.hide = 2; - } else if (templateVariable.hideLabel) { - templateVariable.hide = 1; - } - }); - } - - if (oldVersion < 12) { - // update graph yaxes changes - panelUpgrades.push(function(panel) { - if (panel.type !== 'graph') { - return; - } - if (!panel.grid) { - return; - } - - if (!panel.yaxes) { - panel.yaxes = [ - { - show: panel['y-axis'], - min: panel.grid.leftMin, - max: panel.grid.leftMax, - logBase: panel.grid.leftLogBase, - format: panel.y_formats[0], - label: panel.leftYAxisLabel, - }, - { - show: panel['y-axis'], - min: panel.grid.rightMin, - max: panel.grid.rightMax, - logBase: panel.grid.rightLogBase, - format: panel.y_formats[1], - label: panel.rightYAxisLabel, - }, - ]; - - panel.xaxis = { - show: panel['x-axis'], - }; - - delete panel.grid.leftMin; - delete panel.grid.leftMax; - delete panel.grid.leftLogBase; - delete panel.grid.rightMin; - delete panel.grid.rightMax; - delete panel.grid.rightLogBase; - delete panel.y_formats; - delete panel.leftYAxisLabel; - delete panel.rightYAxisLabel; - delete panel['y-axis']; - delete panel['x-axis']; - } - }); - } - - if (oldVersion < 13) { - // update graph yaxes changes - panelUpgrades.push(function(panel) { - if (panel.type !== 'graph') { - return; - } - if (!panel.grid) { - return; - } - - panel.thresholds = []; - var t1: any = {}, - t2: any = {}; - - if (panel.grid.threshold1 !== null) { - t1.value = panel.grid.threshold1; - if (panel.grid.thresholdLine) { - t1.line = true; - t1.lineColor = panel.grid.threshold1Color; - t1.colorMode = 'custom'; - } else { - t1.fill = true; - t1.fillColor = panel.grid.threshold1Color; - t1.colorMode = 'custom'; - } - } - - if (panel.grid.threshold2 !== null) { - t2.value = panel.grid.threshold2; - if (panel.grid.thresholdLine) { - t2.line = true; - t2.lineColor = panel.grid.threshold2Color; - t2.colorMode = 'custom'; - } else { - t2.fill = true; - t2.fillColor = panel.grid.threshold2Color; - t2.colorMode = 'custom'; - } - } - - if (_.isNumber(t1.value)) { - if (_.isNumber(t2.value)) { - if (t1.value > t2.value) { - t1.op = t2.op = 'lt'; - panel.thresholds.push(t1); - panel.thresholds.push(t2); - } else { - t1.op = t2.op = 'gt'; - panel.thresholds.push(t1); - panel.thresholds.push(t2); - } - } else { - t1.op = 'gt'; - panel.thresholds.push(t1); - } - } - - delete panel.grid.threshold1; - delete panel.grid.threshold1Color; - delete panel.grid.threshold2; - delete panel.grid.threshold2Color; - delete panel.grid.thresholdLine; - }); - } - - if (oldVersion < 14) { - this.graphTooltip = old.sharedCrosshair ? 1 : 0; - } - - if (oldVersion < 16) { - this.upgradeToGridLayout(old); - } - - if (panelUpgrades.length === 0) { - return; - } - - for (j = 0; j < this.panels.length; j++) { - for (k = 0; k < panelUpgrades.length; k++) { - panelUpgrades[k].call(this, this.panels[j]); - } - } - } - - upgradeToGridLayout(old) { - let yPos = 0; - let widthFactor = GRID_COLUMN_COUNT / 12; - //let rowIds = 1000; - // - - if (!old.rows) { - return; - } - - for (let row of old.rows) { - let xPos = 0; - let height: any = row.height || 250; - - // if (this.meta.keepRows) { - // this.panels.push({ - // id: rowIds++, - // type: 'row', - // title: row.title, - // x: 0, - // y: yPos, - // height: 1, - // width: 12 - // }); - // - // yPos += 1; - // } - - if (_.isString(height)) { - height = parseInt(height.replace('px', ''), 10); - } - - const rowGridHeight = Math.ceil(height / GRID_CELL_HEIGHT); - - for (let panel of row.panels) { - const panelWidth = Math.floor(panel.span) * widthFactor; - - // should wrap to next row? - if (xPos + panelWidth >= GRID_COLUMN_COUNT) { - yPos += rowGridHeight; - } - - panel.gridPos = {x: xPos, y: yPos, w: panelWidth, h: rowGridHeight}; - - delete panel.span; - - xPos += panel.gridPos.w; - - this.panels.push(new PanelModel(panel)); - } - - yPos += rowGridHeight; - } + let migrator = new DashboardMigrator(this); + migrator.updateSchema(old); } } diff --git a/public/app/features/dashboard/specs/dashboard_migration.jest.ts b/public/app/features/dashboard/specs/dashboard_migration.jest.ts new file mode 100644 index 00000000000..2c8b5ebcd50 --- /dev/null +++ b/public/app/features/dashboard/specs/dashboard_migration.jest.ts @@ -0,0 +1,341 @@ +import _ from 'lodash'; +import { DashboardModel } from '../dashboard_model'; +import { PanelModel } from '../panel_model'; +import {GRID_CELL_HEIGHT, GRID_CELL_VMARGIN} from 'app/core/constants'; + +jest.mock('app/core/services/context_srv', () => ({})); + +describe('DashboardModel', function() { + 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' }] }, + ], + 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.panels[0]; + singlestat = model.panels[1]; + table = model.panels[2]; + }); + + it('should have title', function() { + expect(model.title).toBe('No Title'); + }); + + it('should have panel id', function() { + expect(graph.id).toBe(1); + }); + + it('should move time and filtering list', function() { + expect(model.time.from).toBe('now-1d'); + expect(model.templating.list[0].allFormat).toBe('glob'); + }); + + it('graphite panel should change name too graph', function() { + expect(graph.type).toBe('graph'); + }); + + it('single stat panel should have two thresholds', function() { + expect(singlestat.thresholds).toBe('20,30'); + }); + + it('queries without refId should get it', function() { + expect(graph.targets[1].refId).toBe('B'); + }); + + it('update legend setting', function() { + expect(graph.legend.show).toBe(true); + }); + + it('move aliasYAxis to series override', function() { + expect(graph.seriesOverrides[0].alias).toBe('test'); + expect(graph.seriesOverrides[0].yaxis).toBe(2); + }); + + it('should move pulldowns to new schema', function() { + expect(model.annotations.list[1].name).toBe('old'); + }); + + it('table panel should only have two thresholds values', function() { + expect(table.styles[0].thresholds[0]).toBe('20'); + expect(table.styles[0].thresholds[1]).toBe('30'); + expect(table.styles[1].thresholds[0]).toBe('200'); + expect(table.styles[1].thresholds[1]).toBe('300'); + }); + + it('graph grid to yaxes options', function() { + expect(graph.yaxes[0].min).toBe(1); + expect(graph.yaxes[0].max).toBe(10); + expect(graph.yaxes[0].format).toBe('kbyte'); + expect(graph.yaxes[0].label).toBe('left label'); + expect(graph.yaxes[0].logBase).toBe(1); + expect(graph.yaxes[1].min).toBe(5); + expect(graph.yaxes[1].max).toBe(15); + expect(graph.yaxes[1].format).toBe('ms'); + expect(graph.yaxes[1].logBase).toBe(2); + + expect(graph.grid.rightMax).toBe(undefined); + expect(graph.grid.rightLogBase).toBe(undefined); + expect(graph.y_formats).toBe(undefined); + }); + + it('dashboard schema version should be set to latest', function() { + expect(model.schemaVersion).toBe(16); + }); + + it('graph thresholds should be migrated', function() { + expect(graph.thresholds.length).toBe(2); + expect(graph.thresholds[0].op).toBe('gt'); + expect(graph.thresholds[0].value).toBe(200); + expect(graph.thresholds[0].fillColor).toBe('yellow'); + expect(graph.thresholds[1].value).toBe(400); + expect(graph.thresholds[1].fillColor).toBe('red'); + }); + }); + + describe('when migrating to the grid layout', function() { + let model; + + beforeEach(function() { + model = { + rows: [] + }; + }); + + it('should create proper grid', function() { + model.rows = [ + createRow({collapse: false, height: 8}, [[6], [6]]) + ]; + let dashboard = new DashboardModel(model); + let panelGridPos = getGridPositions(dashboard); + let expectedGrid = [ + {x: 0, y: 0, w: 12, h: 8}, {x: 12, y: 0, w: 12, h: 8} + ]; + + expect(panelGridPos).toEqual(expectedGrid); + }); + + it('should add special "row" panel if row is collapsed', function() { + model.rows = [ + createRow({collapse: true, height: 8}, [[6], [6]]), + createRow({height: 8}, [[12]]) + ]; + let dashboard = new DashboardModel(model); + let panelGridPos = getGridPositions(dashboard); + let expectedGrid = [ + {x: 0, y: 0, w: 24, h: 8}, // row + {x: 0, y: 1, w: 24, h: 8}, // row + {x: 0, y: 2, w: 24, h: 8} + ]; + + expect(panelGridPos).toEqual(expectedGrid); + }); + + it('should add special "row" panel if row has visible title', function() { + model.rows = [ + createRow({showTitle: true, title: "Row", height: 8}, [[6], [6]]), + createRow({height: 8}, [[12]]) + ]; + let dashboard = new DashboardModel(model); + let panelGridPos = getGridPositions(dashboard); + let expectedGrid = [ + {x: 0, y: 0, w: 24, h: 8}, // row + {x: 0, y: 1, w: 12, h: 8}, {x: 12, y: 1, w: 12, h: 8}, + {x: 0, y: 9, w: 24, h: 8}, // row + {x: 0, y: 10, w: 24, h: 8}, + ]; + + expect(panelGridPos).toEqual(expectedGrid); + }); + + it('should not add "row" panel if row has not visible title or not collapsed', function() { + model.rows = [ + createRow({collapse: true, height: 8}, [[12]]), + createRow({height: 8}, [[12]]), + createRow({height: 8}, [[12], [6], [6]]), + createRow({collapse: true, height: 8}, [[12]]) + ]; + let dashboard = new DashboardModel(model); + let panelGridPos = getGridPositions(dashboard); + let expectedGrid = [ + {x: 0, y: 0, w: 24, h: 8}, // row + {x: 0, y: 1, w: 24, h: 8}, // row + {x: 0, y: 2, w: 24, h: 8}, + {x: 0, y: 10, w: 24, h: 8}, // row + {x: 0, y: 11, w: 24, h: 8}, + {x: 0, y: 19, w: 12, h: 8}, {x: 12, y: 19, w: 12, h: 8}, + {x: 0, y: 27, w: 24, h: 8}, // row + ]; + + expect(panelGridPos).toEqual(expectedGrid); + }); + + it('should add all rows if even one collapsed or titled row is present', function() { + model.rows = [ + createRow({collapse: true, height: 8}, [[6], [6]]), + createRow({height: 8}, [[12]]) + ]; + let dashboard = new DashboardModel(model); + let panelGridPos = getGridPositions(dashboard); + let expectedGrid = [ + {x: 0, y: 0, w: 24, h: 8}, // row + {x: 0, y: 1, w: 24, h: 8}, // row + {x: 0, y: 2, w: 24, h: 8} + ]; + + expect(panelGridPos).toEqual(expectedGrid); + }); + + it('should properly place panels with fixed height', function() { + model.rows = [ + createRow({height: 6}, [[6], [6, 3], [6, 3]]), + createRow({height: 6}, [[4], [4], [4, 3], [4, 3]]) + ]; + let dashboard = new DashboardModel(model); + let panelGridPos = getGridPositions(dashboard); + let expectedGrid = [ + {x: 0, y: 0, w: 12, h: 6}, {x: 12, y: 0, w: 12, h: 3}, {x: 12, y: 3, w: 12, h: 3}, + {x: 0, y: 6, w: 8, h: 6}, {x: 8, y: 6, w: 8, h: 6}, {x: 16, y: 6, w: 8, h: 3}, {x: 16, y: 9, w: 8, h: 3} + ]; + + expect(panelGridPos).toEqual(expectedGrid); + }); + + it('should place panel to the right side of panel having bigger height', function() { + model.rows = [ + createRow({height: 6}, [[4], [2, 3], [4, 6], [2, 3], [2, 3]]) + ]; + let dashboard = new DashboardModel(model); + let panelGridPos = getGridPositions(dashboard); + let expectedGrid = [ + {x: 0, y: 0, w: 8, h: 6}, {x: 8, y: 0, w: 4, h: 3}, + {x: 12, y: 0, w: 8, h: 6}, {x: 20, y: 0, w: 4, h: 3}, {x: 20, y: 3, w: 4, h: 3} + ]; + + expect(panelGridPos).toEqual(expectedGrid); + }); + + it('should fill current row if it possible', function() { + model.rows = [ + createRow({height: 9}, [[4], [2, 3], [4, 6], [2, 3], [2, 3], [8, 3]]) + ]; + let dashboard = new DashboardModel(model); + let panelGridPos = getGridPositions(dashboard); + let expectedGrid = [ + {x: 0, y: 0, w: 8, h: 9}, {x: 8, y: 0, w: 4, h: 3}, {x: 12, y: 0, w: 8, h: 6}, + {x: 20, y: 0, w: 4, h: 3}, {x: 20, y: 3, w: 4, h: 3}, {x: 8, y: 6, w: 16, h: 3} + ]; + + expect(panelGridPos).toEqual(expectedGrid); + }); + + it('should fill current row if it possible (2)', function() { + model.rows = [ + createRow({height: 8}, [[4], [2, 3], [4, 6], [2, 3], [2, 3], [8, 3]]) + ]; + let dashboard = new DashboardModel(model); + let panelGridPos = getGridPositions(dashboard); + let expectedGrid = [ + {x: 0, y: 0, w: 8, h: 8}, {x: 8, y: 0, w: 4, h: 3}, {x: 12, y: 0, w: 8, h: 6}, + {x: 20, y: 0, w: 4, h: 3}, {x: 20, y: 3, w: 4, h: 3}, {x: 8, y: 6, w: 16, h: 3} + ]; + + expect(panelGridPos).toEqual(expectedGrid); + }); + + it('should fill current row if panel height more than row height', function() { + model.rows = [ + createRow({height: 6}, [[4], [2, 3], [4, 8], [2, 3], [2, 3]]) + ]; + let dashboard = new DashboardModel(model); + let panelGridPos = getGridPositions(dashboard); + let expectedGrid = [ + {x: 0, y: 0, w: 8, h: 6}, {x: 8, y: 0, w: 4, h: 3}, + {x: 12, y: 0, w: 8, h: 8}, {x: 20, y: 0, w: 4, h: 3}, {x: 20, y: 3, w: 4, h: 3} + ]; + + expect(panelGridPos).toEqual(expectedGrid); + }); + + it('should wrap panels to multiple rows', function() { + model.rows = [ + createRow({height: 6}, [[6], [6], [12], [6], [3], [3]]) + ]; + let dashboard = new DashboardModel(model); + let panelGridPos = getGridPositions(dashboard); + let expectedGrid = [ + {x: 0, y: 0, w: 12, h: 6}, {x: 12, y: 0, w: 12, h: 6}, + {x: 0, y: 6, w: 24, h: 6}, + {x: 0, y: 12, w: 12, h: 6}, {x: 12, y: 12, w: 6, h: 6}, {x: 18, y: 12, w: 6, h: 6}, + ]; + + expect(panelGridPos).toEqual(expectedGrid); + }); + }); +}); + +function createRow(options, panelDescriptions: any[]) { + const PANEL_HEIGHT_STEP = GRID_CELL_HEIGHT + GRID_CELL_VMARGIN; + let {collapse, height, showTitle, title} = options; + height = height * PANEL_HEIGHT_STEP; + let panels = []; + _.each(panelDescriptions, panelDesc => { + let panel = {span: panelDesc[0]}; + if (panelDesc.length > 1) { + panel['height'] = panelDesc[1] * PANEL_HEIGHT_STEP; + } + panels.push(panel); + }); + let row = {collapse, height, showTitle, title, panels}; + return row; +} + +function getGridPositions(dashboard: DashboardModel) { + return _.map(dashboard.panels, (panel: PanelModel) => { + return panel.gridPos; + }); +} diff --git a/public/app/features/dashboard/specs/dashboard_model.jest.ts b/public/app/features/dashboard/specs/dashboard_model.jest.ts index 65d598859d1..e842d3ea868 100644 --- a/public/app/features/dashboard/specs/dashboard_model.jest.ts +++ b/public/app/features/dashboard/specs/dashboard_model.jest.ts @@ -84,137 +84,6 @@ describe('DashboardModel', function() { }); }); - 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' }] }, - ], - 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.panels[0]; - singlestat = model.panels[1]; - table = model.panels[2]; - }); - - it('should have title', function() { - expect(model.title).toBe('No Title'); - }); - - it('should have panel id', function() { - expect(graph.id).toBe(1); - }); - - it('should move time and filtering list', function() { - expect(model.time.from).toBe('now-1d'); - expect(model.templating.list[0].allFormat).toBe('glob'); - }); - - it('graphite panel should change name too graph', function() { - expect(graph.type).toBe('graph'); - }); - - it('single stat panel should have two thresholds', function() { - expect(singlestat.thresholds).toBe('20,30'); - }); - - it('queries without refId should get it', function() { - expect(graph.targets[1].refId).toBe('B'); - }); - - it('update legend setting', function() { - expect(graph.legend.show).toBe(true); - }); - - it('move aliasYAxis to series override', function() { - expect(graph.seriesOverrides[0].alias).toBe('test'); - expect(graph.seriesOverrides[0].yaxis).toBe(2); - }); - - it('should move pulldowns to new schema', function() { - expect(model.annotations.list[1].name).toBe('old'); - }); - - it('table panel should only have two thresholds values', function() { - expect(table.styles[0].thresholds[0]).toBe('20'); - expect(table.styles[0].thresholds[1]).toBe('30'); - expect(table.styles[1].thresholds[0]).toBe('200'); - expect(table.styles[1].thresholds[1]).toBe('300'); - }); - - it('graph grid to yaxes options', function() { - expect(graph.yaxes[0].min).toBe(1); - expect(graph.yaxes[0].max).toBe(10); - expect(graph.yaxes[0].format).toBe('kbyte'); - expect(graph.yaxes[0].label).toBe('left label'); - expect(graph.yaxes[0].logBase).toBe(1); - expect(graph.yaxes[1].min).toBe(5); - expect(graph.yaxes[1].max).toBe(15); - expect(graph.yaxes[1].format).toBe('ms'); - expect(graph.yaxes[1].logBase).toBe(2); - - expect(graph.grid.rightMax).toBe(undefined); - expect(graph.grid.rightLogBase).toBe(undefined); - expect(graph.y_formats).toBe(undefined); - }); - - it('dashboard schema version should be set to latest', function() { - expect(model.schemaVersion).toBe(16); - }); - - it('graph thresholds should be migrated', function() { - expect(graph.thresholds.length).toBe(2); - expect(graph.thresholds[0].op).toBe('gt'); - expect(graph.thresholds[0].value).toBe(200); - expect(graph.thresholds[0].fillColor).toBe('yellow'); - expect(graph.thresholds[1].value).toBe(400); - expect(graph.thresholds[1].fillColor).toBe('red'); - }); - }); - describe('Given editable false dashboard', function() { var model;