grafana/public/app/features/dashboard/dashboard_model.ts
Patrick O'Carroll 37f9bdfc8c 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.
2018-05-31 15:40:57 +02:00

789 lines
21 KiB
TypeScript

import moment from 'moment';
import _ from 'lodash';
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;
uid: any;
title: any;
autoUpdate: any;
description: any;
tags: any;
style: any;
timezone: any;
editable: any;
graphTooltip: any;
time: any;
originalTime: any;
timepicker: any;
templating: any;
originalTemplating: any;
annotations: any;
refresh: any;
snapshot: any;
schemaVersion: number;
version: number;
revision: number;
links: any;
gnetId: any;
panels: PanelModel[];
// ------------------
// not persisted
// ------------------
// repeat process cycles
iteration: number;
meta: any;
events: Emitter;
static nonPersistedProperties: { [str: string]: boolean } = {
events: true,
meta: true,
panels: true, // needs special handling
templating: true, // needs special handling
};
constructor(data, meta?) {
if (!data) {
data = {};
}
this.events = new Emitter();
this.id = data.id || null;
this.uid = data.uid || null;
this.revision = data.revision;
this.title = data.title || 'No Title';
this.autoUpdate = data.autoUpdate;
this.description = data.description;
this.tags = data.tags || [];
this.style = data.style || 'dark';
this.timezone = data.timezone || '';
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;
this.schemaVersion = data.schemaVersion || 0;
this.version = data.version || 0;
this.links = data.links || [];
this.gnetId = data.gnetId || null;
this.panels = _.map(data.panels || [], panelData => new PanelModel(panelData));
this.initMeta(meta);
this.updateSchema(data);
this.addBuiltInAnnotationQuery();
this.sortPanelsByGridPos();
}
addBuiltInAnnotationQuery() {
let found = false;
for (let item of this.annotations.list) {
if (item.builtIn === 1) {
found = true;
break;
}
}
if (found) {
return;
}
this.annotations.list.unshift({
datasource: '-- Grafana --',
name: 'Annotations & Alerts',
type: 'dashboard',
iconColor: DEFAULT_ANNOTATION_COLOR,
enable: true,
hide: true,
builtIn: 1,
});
}
private initMeta(meta) {
meta = meta || {};
meta.canShare = meta.canShare !== false;
meta.canSave = meta.canSave !== false;
meta.canStar = meta.canStar !== false;
meta.canEdit = meta.canEdit !== false;
meta.showSettings = meta.canEdit;
meta.canMakeEditable = meta.canSave && !this.editable;
if (!this.editable) {
meta.canEdit = false;
meta.canDelete = false;
meta.canSave = false;
}
this.meta = meta;
}
// cleans meta data and other non persistent state
getSaveModelClone(options?) {
let defaults = _.defaults(options || {}, {
saveVariables: false,
saveTimerange: false,
});
// make clone
var copy: any = {};
for (var property in this) {
if (DashboardModel.nonPersistedProperties[property] || !this.hasOwnProperty(property)) {
continue;
}
copy[property] = _.cloneDeep(this[property]);
}
// 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')
.map(panel => panel.getSaveModel())
.value();
// sort by keys
copy = sortByKeys(copy);
return copy;
}
setViewMode(panel: PanelModel, fullscreen: boolean, isEditing: boolean) {
this.meta.fullscreen = fullscreen;
this.meta.isEditing = isEditing && this.meta.canEdit;
panel.setViewMode(fullscreen, this.meta.isEditing);
this.events.emit('view-mode-changed', panel);
}
private ensureListExist(data) {
if (!data) {
data = {};
}
if (!data.list) {
data.list = [];
}
return data;
}
getNextPanelId() {
let max = 0;
for (let panel of this.panels) {
if (panel.id > max) {
max = panel.id;
}
if (panel.collapsed) {
for (let rowPanel of panel.panels) {
if (rowPanel.id > max) {
max = rowPanel.id;
}
}
}
}
return max + 1;
}
forEachPanel(callback) {
for (let i = 0; i < this.panels.length; i++) {
callback(this.panels[i], i);
}
}
getPanelById(id) {
for (let panel of this.panels) {
if (panel.id === id) {
return panel;
}
}
return null;
}
addPanel(panelData) {
panelData.id = this.getNextPanelId();
let panel = new PanelModel(panelData);
this.panels.unshift(panel);
this.sortPanelsByGridPos();
this.events.emit('panel-added', panel);
}
sortPanelsByGridPos() {
this.panels.sort(function(panelA, panelB) {
if (panelA.gridPos.y === panelB.gridPos.y) {
return panelA.gridPos.x - panelB.gridPos.x;
} else {
return panelA.gridPos.y - panelB.gridPos.y;
}
});
}
cleanUpRepeats() {
if (this.snapshot || this.templating.list.length === 0) {
return;
}
this.iteration = (this.iteration || new Date().getTime()) + 1;
let panelsToRemove = [];
// cleanup scopedVars
for (let panel of this.panels) {
delete panel.scopedVars;
}
for (let i = 0; i < this.panels.length; i++) {
let panel = this.panels[i];
if ((!panel.repeat || panel.repeatedByRow) && panel.repeatPanelId && panel.repeatIteration !== this.iteration) {
panelsToRemove.push(panel);
}
}
// remove panels
_.pull(this.panels, ...panelsToRemove);
this.sortPanelsByGridPos();
this.events.emit('repeats-processed');
}
processRepeats() {
if (this.snapshot || this.templating.list.length === 0) {
return;
}
this.cleanUpRepeats();
this.iteration = (this.iteration || new Date().getTime()) + 1;
for (let i = 0; i < this.panels.length; i++) {
let panel = this.panels[i];
if (panel.repeat) {
this.repeatPanel(panel, i);
}
}
this.sortPanelsByGridPos();
this.events.emit('repeats-processed');
}
cleanUpRowRepeats(rowPanels) {
let panelsToRemove = [];
for (let i = 0; i < rowPanels.length; i++) {
let panel = rowPanels[i];
if (!panel.repeat && panel.repeatPanelId) {
panelsToRemove.push(panel);
}
}
_.pull(rowPanels, ...panelsToRemove);
_.pull(this.panels, ...panelsToRemove);
}
processRowRepeats(row: PanelModel) {
if (this.snapshot || this.templating.list.length === 0) {
return;
}
let rowPanels = row.panels;
if (!row.collapsed) {
let rowPanelIndex = _.findIndex(this.panels, p => p.id === row.id);
rowPanels = this.getRowPanels(rowPanelIndex);
}
this.cleanUpRowRepeats(rowPanels);
for (let i = 0; i < rowPanels.length; i++) {
let panel = rowPanels[i];
if (panel.repeat) {
let panelIndex = _.findIndex(this.panels, p => p.id === panel.id);
this.repeatPanel(panel, panelIndex);
}
}
}
getPanelRepeatClone(sourcePanel, valueIndex, sourcePanelIndex) {
// if first clone return source
if (valueIndex === 0) {
return sourcePanel;
}
let clone = new PanelModel(sourcePanel.getSaveModel());
clone.id = this.getNextPanelId();
// insert after source panel + value index
this.panels.splice(sourcePanelIndex + valueIndex, 0, clone);
clone.repeatIteration = this.iteration;
clone.repeatPanelId = sourcePanel.id;
clone.repeat = null;
return clone;
}
getRowRepeatClone(sourceRowPanel, valueIndex, sourcePanelIndex) {
// if first clone return source
if (valueIndex === 0) {
if (!sourceRowPanel.collapsed) {
let rowPanels = this.getRowPanels(sourcePanelIndex);
sourceRowPanel.panels = rowPanels;
}
return sourceRowPanel;
}
let clone = new PanelModel(sourceRowPanel.getSaveModel());
// for row clones we need to figure out panels under row to clone and where to insert clone
let rowPanels, insertPos;
if (sourceRowPanel.collapsed) {
rowPanels = _.cloneDeep(sourceRowPanel.panels);
clone.panels = rowPanels;
// insert copied row after preceding row
insertPos = sourcePanelIndex + valueIndex;
} else {
rowPanels = this.getRowPanels(sourcePanelIndex);
clone.panels = _.map(rowPanels, panel => panel.getSaveModel());
// insert copied row after preceding row's panels
insertPos = sourcePanelIndex + (rowPanels.length + 1) * valueIndex;
}
this.panels.splice(insertPos, 0, clone);
this.updateRepeatedPanelIds(clone);
return clone;
}
repeatPanel(panel: PanelModel, panelIndex: number) {
let variable = _.find(this.templating.list, { name: panel.repeat });
if (!variable) {
return;
}
if (panel.type === 'row') {
this.repeatRow(panel, panelIndex, variable);
return;
}
let selectedOptions = this.getSelectedVariableOptions(variable);
let minWidth = panel.minSpan || 6;
let xPos = 0;
let yPos = panel.gridPos.y;
for (let index = 0; index < selectedOptions.length; index++) {
let option = selectedOptions[index];
let copy;
copy = this.getPanelRepeatClone(panel, index, panelIndex);
copy.scopedVars = copy.scopedVars || {};
copy.scopedVars[variable.name] = option;
if (panel.repeatDirection === REPEAT_DIR_VERTICAL) {
if (index > 0) {
yPos += copy.gridPos.h;
}
copy.gridPos.y = yPos;
} else {
// set width based on how many are selected
// assumed the repeated panels should take up full row width
copy.gridPos.w = Math.max(GRID_COLUMN_COUNT / selectedOptions.length, minWidth);
copy.gridPos.x = xPos;
copy.gridPos.y = yPos;
xPos += copy.gridPos.w;
// handle overflow by pushing down one row
if (xPos + copy.gridPos.w > GRID_COLUMN_COUNT) {
xPos = 0;
yPos += copy.gridPos.h;
}
}
}
// Update gridPos for panels below
let yOffset = yPos - panel.gridPos.y;
if (yOffset > 0) {
let panelBelowIndex = panelIndex + selectedOptions.length;
for (let i = panelBelowIndex; i < this.panels.length; i++) {
this.panels[i].gridPos.y += yOffset;
}
}
}
repeatRow(panel: PanelModel, panelIndex: number, variable) {
let selectedOptions = this.getSelectedVariableOptions(variable);
let yPos = panel.gridPos.y;
function setScopedVars(panel, variableOption) {
panel.scopedVars = panel.scopedVars || {};
panel.scopedVars[variable.name] = variableOption;
}
for (let optionIndex = 0; optionIndex < selectedOptions.length; optionIndex++) {
let option = selectedOptions[optionIndex];
let rowCopy = this.getRowRepeatClone(panel, optionIndex, panelIndex);
setScopedVars(rowCopy, option);
let rowHeight = this.getRowHeight(rowCopy);
let rowPanels = rowCopy.panels || [];
let panelBelowIndex;
if (panel.collapsed) {
// For collapsed row just copy its panels and set scoped vars and proper IDs
_.each(rowPanels, (rowPanel, i) => {
setScopedVars(rowPanel, option);
if (optionIndex > 0) {
this.updateRepeatedPanelIds(rowPanel, true);
}
});
rowCopy.gridPos.y += optionIndex;
yPos += optionIndex;
panelBelowIndex = panelIndex + optionIndex + 1;
} else {
// insert after 'row' panel
let insertPos = panelIndex + (rowPanels.length + 1) * optionIndex + 1;
_.each(rowPanels, (rowPanel, i) => {
setScopedVars(rowPanel, option);
if (optionIndex > 0) {
let cloneRowPanel = new PanelModel(rowPanel);
this.updateRepeatedPanelIds(cloneRowPanel, true);
// For exposed row additionally set proper Y grid position and add it to dashboard panels
cloneRowPanel.gridPos.y += rowHeight * optionIndex;
this.panels.splice(insertPos + i, 0, cloneRowPanel);
}
});
rowCopy.panels = [];
rowCopy.gridPos.y += rowHeight * optionIndex;
yPos += rowHeight;
panelBelowIndex = insertPos + rowPanels.length;
}
// Update gridPos for panels below
for (let i = panelBelowIndex; i < this.panels.length; i++) {
this.panels[i].gridPos.y += yPos;
}
}
}
updateRepeatedPanelIds(panel: PanelModel, repeatedByRow?: boolean) {
panel.repeatPanelId = panel.id;
panel.id = this.getNextPanelId();
panel.repeatIteration = this.iteration;
if (repeatedByRow) {
panel.repeatedByRow = true;
} else {
panel.repeat = null;
}
return panel;
}
getSelectedVariableOptions(variable) {
let selectedOptions;
if (variable.current.text === 'All') {
selectedOptions = variable.options.slice(1, variable.options.length);
} else {
selectedOptions = _.filter(variable.options, { selected: true });
}
return selectedOptions;
}
getRowHeight(rowPanel: PanelModel): number {
if (!rowPanel.panels || rowPanel.panels.length === 0) {
return 0;
}
const rowYPos = rowPanel.gridPos.y;
const positions = _.map(rowPanel.panels, 'gridPos');
const maxPos = _.maxBy(positions, pos => {
return pos.y + pos.h;
});
return maxPos.y + maxPos.h - rowYPos;
}
removePanel(panel: PanelModel) {
var index = _.indexOf(this.panels, panel);
this.panels.splice(index, 1);
this.events.emit('panel-removed', panel);
}
removeRow(row: PanelModel, removePanels: boolean) {
const needToogle = (!removePanels && row.collapsed) || (removePanels && !row.collapsed);
if (needToogle) {
this.toggleRow(row);
}
this.removePanel(row);
}
expandRows() {
for (let i = 0; i < this.panels.length; i++) {
var panel = this.panels[i];
if (panel.type !== 'row') {
continue;
}
if (panel.collapsed) {
this.toggleRow(panel);
}
}
}
collapseRows() {
for (let i = 0; i < this.panels.length; i++) {
var panel = this.panels[i];
if (panel.type !== 'row') {
continue;
}
if (!panel.collapsed) {
this.toggleRow(panel);
}
}
}
setPanelFocus(id) {
this.meta.focusPanelId = id;
}
updateSubmenuVisibility() {
this.meta.submenuEnabled = (() => {
if (this.links.length > 0) {
return true;
}
var visibleVars = _.filter(this.templating.list, variable => variable.hide !== 2);
if (visibleVars.length > 0) {
return true;
}
var visibleAnnotations = _.filter(this.annotations.list, annotation => annotation.hide !== true);
if (visibleAnnotations.length > 0) {
return true;
}
return false;
})();
}
getPanelInfoById(panelId) {
for (let i = 0; i < this.panels.length; i++) {
if (this.panels[i].id === panelId) {
return {
panel: this.panels[i],
index: i,
};
}
}
return null;
}
duplicatePanel(panel) {
const newPanel = panel.getSaveModel();
newPanel.id = this.getNextPanelId();
delete newPanel.repeat;
delete newPanel.repeatIteration;
delete newPanel.repeatPanelId;
delete newPanel.scopedVars;
if (newPanel.alert) {
delete newPanel.thresholds;
}
delete newPanel.alert;
// does it fit to the right?
if (panel.gridPos.x + panel.gridPos.w * 2 <= GRID_COLUMN_COUNT) {
newPanel.gridPos.x += panel.gridPos.w;
} else {
// add below
newPanel.gridPos.y += panel.gridPos.h;
}
this.addPanel(newPanel);
return newPanel;
}
formatDate(date, format?) {
date = moment.isMoment(date) ? date : moment(date);
format = format || 'YYYY-MM-DD HH:mm:ss';
let timezone = this.getTimezone();
return timezone === 'browser' ? moment(date).format(format) : moment.utc(date).format(format);
}
destroy() {
this.events.removeAllListeners();
for (let panel of this.panels) {
panel.destroy();
}
}
toggleRow(row: PanelModel) {
let rowIndex = _.indexOf(this.panels, row);
if (row.collapsed) {
row.collapsed = false;
let hasRepeat = _.some(row.panels, p => p.repeat);
if (row.panels.length > 0) {
// Use first panel to figure out if it was moved or pushed
let firstPanel = row.panels[0];
let yDiff = firstPanel.gridPos.y - (row.gridPos.y + row.gridPos.h);
// start inserting after row
let insertPos = rowIndex + 1;
// y max will represent the bottom y pos after all panels have been added
// needed to know home much panels below should be pushed down
let yMax = row.gridPos.y;
for (let panel of row.panels) {
// make sure y is adjusted (in case row moved while collapsed)
// console.log('yDiff', yDiff);
panel.gridPos.y -= yDiff;
// insert after row
this.panels.splice(insertPos, 0, new PanelModel(panel));
// update insert post and y max
insertPos += 1;
yMax = Math.max(yMax, panel.gridPos.y + panel.gridPos.h);
}
const pushDownAmount = yMax - row.gridPos.y - 1;
// push panels below down
for (let panelIndex = insertPos; panelIndex < this.panels.length; panelIndex++) {
this.panels[panelIndex].gridPos.y += pushDownAmount;
}
row.panels = [];
if (hasRepeat) {
this.processRowRepeats(row);
}
}
// sort panels
this.sortPanelsByGridPos();
// emit change event
this.events.emit('row-expanded');
return;
}
let rowPanels = this.getRowPanels(rowIndex);
// remove panels
_.pull(this.panels, ...rowPanels);
// save panel models inside row panel
row.panels = _.map(rowPanels, panel => panel.getSaveModel());
row.collapsed = true;
// emit change event
this.events.emit('row-collapsed');
}
/**
* Will return all panels after rowIndex until it encounters another row
*/
getRowPanels(rowIndex: number): PanelModel[] {
let rowPanels = [];
for (let index = rowIndex + 1; index < this.panels.length; index++) {
let panel = this.panels[index];
// break when encountering another row
if (panel.type === 'row') {
break;
}
// this panel must belong to row
rowPanels.push(panel);
}
return rowPanels;
}
on(eventName, callback) {
this.events.on(eventName, callback);
}
off(eventName, callback?) {
this.events.off(eventName, callback);
}
cycleGraphTooltip() {
this.graphTooltip = (this.graphTooltip + 1) % 3;
}
sharedTooltipModeEnabled() {
return this.graphTooltip > 0;
}
sharedCrosshairModeOnly() {
return this.graphTooltip === 1;
}
getRelativeTime(date) {
date = moment.isMoment(date) ? date : moment(date);
return this.timezone === 'browser' ? moment(date).fromNow() : moment.utc(date).fromNow();
}
getNextQueryLetter(panel) {
var letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
return _.find(letters, function(refId) {
return _.every(panel.targets, function(other) {
return other.refId !== refId;
});
});
}
isTimezoneUtc() {
return this.getTimezone() === 'utc';
}
getTimezone() {
return this.timezone ? this.timezone : contextSrv.user.timezone;
}
private updateSchema(old) {
let migrator = new DashboardMigrator(this);
migrator.updateSchema(old);
}
}