The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
grafana/public/app/features/dashboard/state/DashboardModel.ts

965 lines
26 KiB

// Libaries
import _ from 'lodash';
// Constants
import { DEFAULT_ANNOTATION_COLOR } from '@grafana/ui';
import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT, REPEAT_DIR_VERTICAL } from 'app/core/constants';
// Utils & Services
import { Emitter } from 'app/core/utils/emitter';
import { contextSrv } from 'app/core/services/context_srv';
import sortByKeys from 'app/core/utils/sort_by_keys';
// Types
import { GridPos, panelAdded, PanelModel, panelRemoved } from './PanelModel';
import { DashboardMigrator } from './DashboardMigrator';
import { AppEvent, dateTime, DateTimeInput, isDateTime, PanelEvents, TimeRange, TimeZone, toUtc } from '@grafana/data';
import { UrlQueryValue } from '@grafana/runtime';
import { CoreEvents, DashboardMeta, KIOSK_MODE_TV } from 'app/types';
export interface CloneOptions {
saveVariables?: boolean;
saveTimerange?: boolean;
message?: string;
}
export class DashboardModel {
id: any;
uid: string;
title: string;
autoUpdate: any;
description: any;
tags: any;
style: any;
timezone: any;
editable: any;
graphTooltip: any;
time: any;
private originalTime: any;
timepicker: any;
templating: { list: any[] };
private originalTemplating: any;
annotations: { list: 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: DashboardMeta;
events: Emitter;
static nonPersistedProperties: { [str: string]: boolean } = {
events: true,
meta: true,
panels: true, // needs special handling
templating: true, // needs special handling
originalTime: true,
originalTemplating: true,
};
constructor(data: any, meta?: DashboardMeta) {
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.timepicker = data.timepicker || {};
this.templating = this.ensureListExist(data.templating);
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: any) => new PanelModel(panelData));
this.resetOriginalVariables();
this.resetOriginalTime();
this.initMeta(meta);
this.updateSchema(data);
this.addBuiltInAnnotationQuery();
this.sortPanelsByGridPos();
}
Create annotations (#8197) * annotations: add 25px space for events section * annotations: restored create annotation action * annotations: able to use fa icons as event markers * annotations: initial emoji support from twemoji lib * annotations: adjust fa icon position * annotations: initial emoji picker * annotation: include user info into annotation requests * annotation: add icon info * annotation: display user info in tooltip * annotation: fix region saving * annotation: initial region markers * annotation: fix region clearing (add flot-temp-elem class) * annotation: adjust styles a bit * annotations: minor fixes * annoations: removed userId look in loop, need a sql join or a user cache for this * annotation: fix invisible events * lib: changed twitter emoij lib to be npm dependency * annotation: add icon picker to Add Annotation dialog * annotation: save icon to annotation table * annotation: able to set custom icon for annotation added by user * annotations: fix emoji after library upgrade (switch to 72px) * emoji: temporary remove bad code points * annotations: improve icon picker * annotations: icon show icon picker at the top * annotations: use svg for emoji * annotations: fix region drawing when add annotation editor opened * annotations: use flot lib for drawing region fill * annotations: move regions building into event_manager * annotations: don't draw additional space if no events are got * annotations: deduplicate events * annotations: properly render cut regions * annotations: fix cut region building * annotations: refactor * annotations: adjust event section size * add-annotations: fix undefined default icon * create-annotations: edit event (frontend part) * fixed bug causes error when hover event marker * create-annotations: update event (backend) * ignore grafana-server debug binary in git (created VS Code) * create-annotations: use PUT request for updating annotation. * create-annotations: fixed time format when editing existing event * create-annotations: support for region update * create-annotations: fix bug with limit and event type * create-annotations: delete annotation * create-annotations: show only selected icon in edit mode * create-annotations: show event editor only for users with at least Editor role * create-annotations: handle double-sized emoji codepoints * create-annotations: refactor use CP_SEPARATOR from emojiDef * create-annotations: update emoji list, add categories. * create-annotations: copy SVG emoji into public/vendor/npm and use it as a base path * create-annotations: initial tabs for emoji picker * emoji-picker: adjust styles * emoji-picker: minor refactor * emoji-picker: refactor - rename and move into one directory * emoji-picker: build emoji elements on app load, not on picker open * emoji-picker: fix emoji searching * emoji-picker: refactor * emoji-picker: capitalize category name * emoji-picker: refactor move buildEmojiElem() into emoji_converter.ts for future reuse. * jquery.flot.events: refactor use buildEmojiElem() for making emojis, remove unused code for font awesome based icons. * emoji_converter: handle converting error * tech: updated * merged with master * shore: clean up some stuff * annotation: wip tags * annotation: filtering by tags * tags: parse out spaces etc. from a tags string * annotations: use tagsinput component for tag filtering * annotation: wip work on how we query alert & panel annotations * annotations: support for updating tags in an annotation * linting * annotations: work on unifying how alert history annotations and manual panel annotations are created * tslint: fixes * tags: create tag on blur as well Currently, the tags directive only creates the tag when the user presses enter. This change means the tag is created on blur as well (when the user clicks outside the input field). * annotations: fix update after refactoring * annotations: progress on how alert annotations are fetched * annotations: minor progress * annotations: progress * annotation: minor progress * annotations: move tag parsing from tooltip to ds Instead of parsing a tag string into an array in the annotation_tooltip class, this moves the parsing to the datasources. InfluxDB ds already does that parsing. Graphite now has it. * annotations: more work on querying * annotations: change from tags as string to array when saving in the db and in the api. * annotations: delete tag link if removed on edit * annotation: more work on depricating annotation title * annotations: delete tag links on delete * annotations: fix for find * annotation: added user to annotation tooltip and added alertName to annoation dto * annotations: use id from route instead from cmd for updating * annotations: http api docs * create annotation: last edits * annotations: minor fix for querying annotations before dashboard saved * annotations: fix for popover placement when legend is on the side (and doubel render pass is causing original marker to be removed) * annotations: changing how the built in query gets added * annotation: added time to header in edit mode * tests: fixed jshint built issue
8 years ago
addBuiltInAnnotationQuery() {
let found = false;
for (const item of this.annotations.list) {
Create annotations (#8197) * annotations: add 25px space for events section * annotations: restored create annotation action * annotations: able to use fa icons as event markers * annotations: initial emoji support from twemoji lib * annotations: adjust fa icon position * annotations: initial emoji picker * annotation: include user info into annotation requests * annotation: add icon info * annotation: display user info in tooltip * annotation: fix region saving * annotation: initial region markers * annotation: fix region clearing (add flot-temp-elem class) * annotation: adjust styles a bit * annotations: minor fixes * annoations: removed userId look in loop, need a sql join or a user cache for this * annotation: fix invisible events * lib: changed twitter emoij lib to be npm dependency * annotation: add icon picker to Add Annotation dialog * annotation: save icon to annotation table * annotation: able to set custom icon for annotation added by user * annotations: fix emoji after library upgrade (switch to 72px) * emoji: temporary remove bad code points * annotations: improve icon picker * annotations: icon show icon picker at the top * annotations: use svg for emoji * annotations: fix region drawing when add annotation editor opened * annotations: use flot lib for drawing region fill * annotations: move regions building into event_manager * annotations: don't draw additional space if no events are got * annotations: deduplicate events * annotations: properly render cut regions * annotations: fix cut region building * annotations: refactor * annotations: adjust event section size * add-annotations: fix undefined default icon * create-annotations: edit event (frontend part) * fixed bug causes error when hover event marker * create-annotations: update event (backend) * ignore grafana-server debug binary in git (created VS Code) * create-annotations: use PUT request for updating annotation. * create-annotations: fixed time format when editing existing event * create-annotations: support for region update * create-annotations: fix bug with limit and event type * create-annotations: delete annotation * create-annotations: show only selected icon in edit mode * create-annotations: show event editor only for users with at least Editor role * create-annotations: handle double-sized emoji codepoints * create-annotations: refactor use CP_SEPARATOR from emojiDef * create-annotations: update emoji list, add categories. * create-annotations: copy SVG emoji into public/vendor/npm and use it as a base path * create-annotations: initial tabs for emoji picker * emoji-picker: adjust styles * emoji-picker: minor refactor * emoji-picker: refactor - rename and move into one directory * emoji-picker: build emoji elements on app load, not on picker open * emoji-picker: fix emoji searching * emoji-picker: refactor * emoji-picker: capitalize category name * emoji-picker: refactor move buildEmojiElem() into emoji_converter.ts for future reuse. * jquery.flot.events: refactor use buildEmojiElem() for making emojis, remove unused code for font awesome based icons. * emoji_converter: handle converting error * tech: updated * merged with master * shore: clean up some stuff * annotation: wip tags * annotation: filtering by tags * tags: parse out spaces etc. from a tags string * annotations: use tagsinput component for tag filtering * annotation: wip work on how we query alert & panel annotations * annotations: support for updating tags in an annotation * linting * annotations: work on unifying how alert history annotations and manual panel annotations are created * tslint: fixes * tags: create tag on blur as well Currently, the tags directive only creates the tag when the user presses enter. This change means the tag is created on blur as well (when the user clicks outside the input field). * annotations: fix update after refactoring * annotations: progress on how alert annotations are fetched * annotations: minor progress * annotations: progress * annotation: minor progress * annotations: move tag parsing from tooltip to ds Instead of parsing a tag string into an array in the annotation_tooltip class, this moves the parsing to the datasources. InfluxDB ds already does that parsing. Graphite now has it. * annotations: more work on querying * annotations: change from tags as string to array when saving in the db and in the api. * annotations: delete tag link if removed on edit * annotation: more work on depricating annotation title * annotations: delete tag links on delete * annotations: fix for find * annotation: added user to annotation tooltip and added alertName to annoation dto * annotations: use id from route instead from cmd for updating * annotations: http api docs * create annotation: last edits * annotations: minor fix for querying annotations before dashboard saved * annotations: fix for popover placement when legend is on the side (and doubel render pass is causing original marker to be removed) * annotations: changing how the built in query gets added * annotation: added time to header in edit mode * tests: fixed jshint built issue
8 years ago
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,
Create annotations (#8197) * annotations: add 25px space for events section * annotations: restored create annotation action * annotations: able to use fa icons as event markers * annotations: initial emoji support from twemoji lib * annotations: adjust fa icon position * annotations: initial emoji picker * annotation: include user info into annotation requests * annotation: add icon info * annotation: display user info in tooltip * annotation: fix region saving * annotation: initial region markers * annotation: fix region clearing (add flot-temp-elem class) * annotation: adjust styles a bit * annotations: minor fixes * annoations: removed userId look in loop, need a sql join or a user cache for this * annotation: fix invisible events * lib: changed twitter emoij lib to be npm dependency * annotation: add icon picker to Add Annotation dialog * annotation: save icon to annotation table * annotation: able to set custom icon for annotation added by user * annotations: fix emoji after library upgrade (switch to 72px) * emoji: temporary remove bad code points * annotations: improve icon picker * annotations: icon show icon picker at the top * annotations: use svg for emoji * annotations: fix region drawing when add annotation editor opened * annotations: use flot lib for drawing region fill * annotations: move regions building into event_manager * annotations: don't draw additional space if no events are got * annotations: deduplicate events * annotations: properly render cut regions * annotations: fix cut region building * annotations: refactor * annotations: adjust event section size * add-annotations: fix undefined default icon * create-annotations: edit event (frontend part) * fixed bug causes error when hover event marker * create-annotations: update event (backend) * ignore grafana-server debug binary in git (created VS Code) * create-annotations: use PUT request for updating annotation. * create-annotations: fixed time format when editing existing event * create-annotations: support for region update * create-annotations: fix bug with limit and event type * create-annotations: delete annotation * create-annotations: show only selected icon in edit mode * create-annotations: show event editor only for users with at least Editor role * create-annotations: handle double-sized emoji codepoints * create-annotations: refactor use CP_SEPARATOR from emojiDef * create-annotations: update emoji list, add categories. * create-annotations: copy SVG emoji into public/vendor/npm and use it as a base path * create-annotations: initial tabs for emoji picker * emoji-picker: adjust styles * emoji-picker: minor refactor * emoji-picker: refactor - rename and move into one directory * emoji-picker: build emoji elements on app load, not on picker open * emoji-picker: fix emoji searching * emoji-picker: refactor * emoji-picker: capitalize category name * emoji-picker: refactor move buildEmojiElem() into emoji_converter.ts for future reuse. * jquery.flot.events: refactor use buildEmojiElem() for making emojis, remove unused code for font awesome based icons. * emoji_converter: handle converting error * tech: updated * merged with master * shore: clean up some stuff * annotation: wip tags * annotation: filtering by tags * tags: parse out spaces etc. from a tags string * annotations: use tagsinput component for tag filtering * annotation: wip work on how we query alert & panel annotations * annotations: support for updating tags in an annotation * linting * annotations: work on unifying how alert history annotations and manual panel annotations are created * tslint: fixes * tags: create tag on blur as well Currently, the tags directive only creates the tag when the user presses enter. This change means the tag is created on blur as well (when the user clicks outside the input field). * annotations: fix update after refactoring * annotations: progress on how alert annotations are fetched * annotations: minor progress * annotations: progress * annotation: minor progress * annotations: move tag parsing from tooltip to ds Instead of parsing a tag string into an array in the annotation_tooltip class, this moves the parsing to the datasources. InfluxDB ds already does that parsing. Graphite now has it. * annotations: more work on querying * annotations: change from tags as string to array when saving in the db and in the api. * annotations: delete tag link if removed on edit * annotation: more work on depricating annotation title * annotations: delete tag links on delete * annotations: fix for find * annotation: added user to annotation tooltip and added alertName to annoation dto * annotations: use id from route instead from cmd for updating * annotations: http api docs * create annotation: last edits * annotations: minor fix for querying annotations before dashboard saved * annotations: fix for popover placement when legend is on the side (and doubel render pass is causing original marker to be removed) * annotations: changing how the built in query gets added * annotation: added time to header in edit mode * tests: fixed jshint built issue
8 years ago
enable: true,
hide: true,
builtIn: 1,
Create annotations (#8197) * annotations: add 25px space for events section * annotations: restored create annotation action * annotations: able to use fa icons as event markers * annotations: initial emoji support from twemoji lib * annotations: adjust fa icon position * annotations: initial emoji picker * annotation: include user info into annotation requests * annotation: add icon info * annotation: display user info in tooltip * annotation: fix region saving * annotation: initial region markers * annotation: fix region clearing (add flot-temp-elem class) * annotation: adjust styles a bit * annotations: minor fixes * annoations: removed userId look in loop, need a sql join or a user cache for this * annotation: fix invisible events * lib: changed twitter emoij lib to be npm dependency * annotation: add icon picker to Add Annotation dialog * annotation: save icon to annotation table * annotation: able to set custom icon for annotation added by user * annotations: fix emoji after library upgrade (switch to 72px) * emoji: temporary remove bad code points * annotations: improve icon picker * annotations: icon show icon picker at the top * annotations: use svg for emoji * annotations: fix region drawing when add annotation editor opened * annotations: use flot lib for drawing region fill * annotations: move regions building into event_manager * annotations: don't draw additional space if no events are got * annotations: deduplicate events * annotations: properly render cut regions * annotations: fix cut region building * annotations: refactor * annotations: adjust event section size * add-annotations: fix undefined default icon * create-annotations: edit event (frontend part) * fixed bug causes error when hover event marker * create-annotations: update event (backend) * ignore grafana-server debug binary in git (created VS Code) * create-annotations: use PUT request for updating annotation. * create-annotations: fixed time format when editing existing event * create-annotations: support for region update * create-annotations: fix bug with limit and event type * create-annotations: delete annotation * create-annotations: show only selected icon in edit mode * create-annotations: show event editor only for users with at least Editor role * create-annotations: handle double-sized emoji codepoints * create-annotations: refactor use CP_SEPARATOR from emojiDef * create-annotations: update emoji list, add categories. * create-annotations: copy SVG emoji into public/vendor/npm and use it as a base path * create-annotations: initial tabs for emoji picker * emoji-picker: adjust styles * emoji-picker: minor refactor * emoji-picker: refactor - rename and move into one directory * emoji-picker: build emoji elements on app load, not on picker open * emoji-picker: fix emoji searching * emoji-picker: refactor * emoji-picker: capitalize category name * emoji-picker: refactor move buildEmojiElem() into emoji_converter.ts for future reuse. * jquery.flot.events: refactor use buildEmojiElem() for making emojis, remove unused code for font awesome based icons. * emoji_converter: handle converting error * tech: updated * merged with master * shore: clean up some stuff * annotation: wip tags * annotation: filtering by tags * tags: parse out spaces etc. from a tags string * annotations: use tagsinput component for tag filtering * annotation: wip work on how we query alert & panel annotations * annotations: support for updating tags in an annotation * linting * annotations: work on unifying how alert history annotations and manual panel annotations are created * tslint: fixes * tags: create tag on blur as well Currently, the tags directive only creates the tag when the user presses enter. This change means the tag is created on blur as well (when the user clicks outside the input field). * annotations: fix update after refactoring * annotations: progress on how alert annotations are fetched * annotations: minor progress * annotations: progress * annotation: minor progress * annotations: move tag parsing from tooltip to ds Instead of parsing a tag string into an array in the annotation_tooltip class, this moves the parsing to the datasources. InfluxDB ds already does that parsing. Graphite now has it. * annotations: more work on querying * annotations: change from tags as string to array when saving in the db and in the api. * annotations: delete tag link if removed on edit * annotation: more work on depricating annotation title * annotations: delete tag links on delete * annotations: fix for find * annotation: added user to annotation tooltip and added alertName to annoation dto * annotations: use id from route instead from cmd for updating * annotations: http api docs * create annotation: last edits * annotations: minor fix for querying annotations before dashboard saved * annotations: fix for popover placement when legend is on the side (and doubel render pass is causing original marker to be removed) * annotations: changing how the built in query gets added * annotation: added time to header in edit mode * tests: fixed jshint built issue
8 years ago
});
}
private initMeta(meta: DashboardMeta) {
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;
meta.fullscreen = false;
meta.isEditing = false;
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?: CloneOptions) {
const defaults = _.defaults(options || {}, {
saveVariables: true,
saveTimerange: true,
});
// make clone
let copy: any = {};
for (const property in this) {
if (DashboardModel.nonPersistedProperties[property] || !this.hasOwnProperty(property)) {
continue;
}
copy[property] = _.cloneDeep(this[property]);
}
// get variable save models
copy.templating = {
list: _.map(this.templating.list, (variable: any) =>
variable.getSaveModel ? variable.getSaveModel() : variable
),
};
if (!defaults.saveVariables) {
for (let i = 0; i < copy.templating.list.length; i++) {
const current = copy.templating.list[i];
const original: any = _.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;
}
}
}
if (!defaults.saveTimerange) {
copy.time = this.originalTime;
}
// get panel save models
copy.panels = _.chain(this.panels)
.filter((panel: PanelModel) => panel.type !== 'add-panel')
.map((panel: PanelModel) => 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(PanelEvents.viewModeChanged, panel);
}
timeRangeUpdated(timeRange: TimeRange) {
this.events.emit(CoreEvents.timeRangeUpdated, timeRange);
}
startRefresh() {
this.events.emit(PanelEvents.refresh);
for (const panel of this.panels) {
if (!this.otherPanelInFullscreen(panel)) {
panel.refresh();
}
}
}
render() {
this.events.emit(PanelEvents.render);
for (const panel of this.panels) {
panel.render();
}
}
panelInitialized(panel: PanelModel) {
panel.initialized();
// In new panel edit there is no need to trigger refresh as editor retrieves last results from the query runner
// as an initial value
if (!this.otherPanelInFullscreen(panel) && !panel.isNewEdit) {
panel.refresh();
}
}
otherPanelInFullscreen(panel: PanelModel) {
return this.meta.fullscreen && !panel.fullscreen;
}
private ensureListExist(data: any) {
if (!data) {
data = {};
}
if (!data.list) {
data.list = [];
}
return data;
}
getNextPanelId() {
let max = 0;
for (const panel of this.panels) {
if (panel.id > max) {
max = panel.id;
}
if (panel.collapsed) {
for (const rowPanel of panel.panels) {
if (rowPanel.id > max) {
max = rowPanel.id;
}
}
}
}
return max + 1;
}
forEachPanel(callback: (panel: PanelModel, index: number) => void) {
for (let i = 0; i < this.panels.length; i++) {
callback(this.panels[i], i);
}
}
getPanelById(id: number): PanelModel {
for (const panel of this.panels) {
if (panel.id === id) {
return panel;
}
}
return null;
}
canEditPanel(panel?: PanelModel): boolean {
return this.meta.canEdit && panel && !panel.repeatPanelId;
}
canEditPanelById(id: number): boolean {
return this.canEditPanel(this.getPanelById(id));
}
addPanel(panelData: any) {
panelData.id = this.getNextPanelId();
const panel = new PanelModel(panelData);
this.panels.unshift(panel);
this.sortPanelsByGridPos();
this.events.emit(panelAdded, panel);
}
sortPanelsByGridPos() {
this.panels.sort((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;
const panelsToRemove = [];
// cleanup scopedVars
for (const panel of this.panels) {
delete panel.scopedVars;
}
for (let i = 0; i < this.panels.length; i++) {
const panel = this.panels[i];
if ((!panel.repeat || panel.repeatedByRow) && panel.repeatPanelId && panel.repeatIteration !== this.iteration) {
panelsToRemove.push(panel);
}
}
// remove panels
_.pull(this.panels, ...panelsToRemove);
panelsToRemove.map(p => p.destroy());
this.sortPanelsByGridPos();
this.events.emit(CoreEvents.repeatsProcessed);
}
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++) {
const panel = this.panels[i];
if (panel.repeat) {
this.repeatPanel(panel, i);
}
}
this.sortPanelsByGridPos();
this.events.emit(CoreEvents.repeatsProcessed);
}
cleanUpRowRepeats(rowPanels: PanelModel[]) {
const panelsToRemove = [];
for (let i = 0; i < rowPanels.length; i++) {
const 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) {
const rowPanelIndex = _.findIndex(this.panels, (p: PanelModel) => p.id === row.id);
rowPanels = this.getRowPanels(rowPanelIndex);
}
this.cleanUpRowRepeats(rowPanels);
for (let i = 0; i < rowPanels.length; i++) {
const panel = rowPanels[i];
if (panel.repeat) {
const panelIndex = _.findIndex(this.panels, (p: PanelModel) => p.id === panel.id);
this.repeatPanel(panel, panelIndex);
}
}
}
getPanelRepeatClone(sourcePanel: PanelModel, valueIndex: number, sourcePanelIndex: number) {
// if first clone return source
if (valueIndex === 0) {
return sourcePanel;
}
const 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: PanelModel, valueIndex: number, sourcePanelIndex: number) {
// if first clone return source
if (valueIndex === 0) {
if (!sourceRowPanel.collapsed) {
const rowPanels = this.getRowPanels(sourcePanelIndex);
sourceRowPanel.panels = rowPanels;
}
return sourceRowPanel;
}
const 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: PanelModel[], insertPos: number;
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: PanelModel) => 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) {
const variable: any = _.find(this.templating.list, { name: panel.repeat } as any);
if (!variable) {
return;
}
if (panel.type === 'row') {
this.repeatRow(panel, panelIndex, variable);
return;
}
const selectedOptions = this.getSelectedVariableOptions(variable);
const maxPerRow = panel.maxPerRow || 4;
let xPos = 0;
let yPos = panel.gridPos.y;
for (let index = 0; index < selectedOptions.length; index++) {
const 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, GRID_COLUMN_COUNT / maxPerRow);
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
const yOffset = yPos - panel.gridPos.y;
if (yOffset > 0) {
const 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: any) {
const selectedOptions = this.getSelectedVariableOptions(variable);
let yPos = panel.gridPos.y;
function setScopedVars(panel: PanelModel, variableOption: any) {
panel.scopedVars = panel.scopedVars || {};
panel.scopedVars[variable.name] = variableOption;
}
for (let optionIndex = 0; optionIndex < selectedOptions.length; optionIndex++) {
const option = selectedOptions[optionIndex];
const rowCopy = this.getRowRepeatClone(panel, optionIndex, panelIndex);
setScopedVars(rowCopy, option);
const rowHeight = this.getRowHeight(rowCopy);
const 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: PanelModel, i: number) => {
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
const insertPos = panelIndex + (rowPanels.length + 1) * optionIndex + 1;
_.each(rowPanels, (rowPanel: PanelModel, i: number) => {
setScopedVars(rowPanel, option);
if (optionIndex > 0) {
const 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: any) {
let selectedOptions: any[];
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: GridPos) => {
return pos.y + pos.h;
});
return maxPos.y + maxPos.h - rowYPos;
}
removePanel(panel: PanelModel) {
const index = _.indexOf(this.panels, panel);
this.panels.splice(index, 1);
this.events.emit(panelRemoved, 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++) {
const 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++) {
const panel = this.panels[i];
if (panel.type !== 'row') {
continue;
}
if (!panel.collapsed) {
this.toggleRow(panel);
}
}
}
setPanelFocus(id: number) {
this.meta.focusPanelId = id;
}
updateSubmenuVisibility() {
this.meta.submenuEnabled = (() => {
if (this.links.length > 0) {
return true;
}
const visibleVars = _.filter(this.templating.list, (variable: any) => variable.hide !== 2);
if (visibleVars.length > 0) {
return true;
}
const visibleAnnotations = _.filter(this.annotations.list, (annotation: any) => annotation.hide !== true);
if (visibleAnnotations.length > 0) {
return true;
}
return false;
})();
this.events.emit(CoreEvents.submenuVisibilityChanged, this.meta.submenuEnabled);
}
getPanelInfoById(panelId: number) {
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: PanelModel) {
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: DateTimeInput, format?: string) {
date = isDateTime(date) ? date : dateTime(date);
format = format || 'YYYY-MM-DD HH:mm:ss';
const timezone = this.getTimezone();
return timezone === 'browser' ? dateTime(date).format(format) : toUtc(date).format(format);
}
destroy() {
this.events.removeAllListeners();
for (const panel of this.panels) {
panel.destroy();
}
}
8 years ago
toggleRow(row: PanelModel) {
const rowIndex = _.indexOf(this.panels, row);
8 years ago
if (row.collapsed) {
row.collapsed = false;
const hasRepeat = _.some(row.panels as PanelModel[], (p: PanelModel) => p.repeat);
8 years ago
if (row.panels.length > 0) {
// Use first panel to figure out if it was moved or pushed
const firstPanel = row.panels[0];
const yDiff = firstPanel.gridPos.y - (row.gridPos.y + row.gridPos.h);
8 years ago
// 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;
8 years ago
for (const panel of row.panels) {
8 years ago
// make sure y is adjusted (in case row moved while collapsed)
// console.log('yDiff', yDiff);
8 years ago
panel.gridPos.y -= yDiff;
// insert after row
this.panels.splice(insertPos, 0, new PanelModel(panel));
// update insert post and y max
8 years ago
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;
8 years ago
}
8 years ago
row.panels = [];
if (hasRepeat) {
this.processRowRepeats(row);
}
}
8 years ago
// sort panels
this.sortPanelsByGridPos();
8 years ago
// emit change event
this.events.emit(CoreEvents.rowExpanded);
8 years ago
return;
}
const rowPanels = this.getRowPanels(rowIndex);
// remove panels
_.pull(this.panels, ...rowPanels);
// save panel models inside row panel
row.panels = _.map(rowPanels, (panel: PanelModel) => panel.getSaveModel());
row.collapsed = true;
// emit change event
this.events.emit(CoreEvents.rowCollapsed);
}
/**
* Will return all panels after rowIndex until it encounters another row
*/
getRowPanels(rowIndex: number): PanelModel[] {
const rowPanels = [];
for (let index = rowIndex + 1; index < this.panels.length; index++) {
const panel = this.panels[index];
8 years ago
// break when encountering another row
if (panel.type === 'row') {
8 years ago
break;
}
8 years ago
// this panel must belong to row
rowPanels.push(panel);
}
return rowPanels;
}
on<T>(event: AppEvent<T>, callback: (payload?: T) => void) {
this.events.on(event, callback);
}
off<T>(event: AppEvent<T>, callback?: (payload?: T) => void) {
this.events.off(event, callback);
}
cycleGraphTooltip() {
this.graphTooltip = (this.graphTooltip + 1) % 3;
}
sharedTooltipModeEnabled() {
return this.graphTooltip > 0;
}
sharedCrosshairModeOnly() {
return this.graphTooltip === 1;
}
getRelativeTime(date: DateTimeInput) {
date = isDateTime(date) ? date : dateTime(date);
return this.timezone === 'browser' ? dateTime(date).fromNow() : toUtc(date).fromNow();
}
isTimezoneUtc() {
return this.getTimezone() === 'utc';
}
isSnapshot() {
return this.snapshot !== undefined;
}
TimePicker: New time picker dropdown & custom range UI (#16811) * feat: Add new picker to DashNavTimeControls * chore: noImplicitAny limit reached * chore: noImplicityAny fix * chore: Add momentUtc helper to avoid the isUtc conditionals * chore: Move getRaw from Explore's time picker to grafana/ui utils and rename to getRawRange * feat: Use helper functions to convert utc to browser time * fix: Dont Select current value when pressing tab when using Time Picker * fix: Add tabIndex to time range inputs so tab works smoothly and prevent mouseDown event to propagate to react-select * fix: Add spacing to custom range labels * fix: Updated snapshot * fix: Re-adding getRaw() temporary to fix the build * fix: Disable scroll event in Popper when we're using the TimePicker so the popup wont "follow" the menu * fix: Move all "Last xxxx" quick ranges to the menu and show a "UTC" text when applicable * fix: Add zoom functionality * feat: Add logic to mark selected option as active * fix: Add tooltip to zoom button * fix: lint fix after rebase * chore: Remove old time picker from DashNav * TimePicker: minor design update * chore: Move all time picker quick ranges to the menu * fix: Remove the popover border-right, since the quick ranges are gone * chore: Remove function not in use * Fix: Close time picker on resize event * Fix: Remove border bottom * Fix: Use fa icons on prev/next arrows * Fix: Pass ref from TimePicker to TimePickerOptionGroup so the popover will align as it should * Fix: time picker ui adjustments to get better touch area on buttons * Fix: Dont increase line height on large screens * TimePicker: style updates * Fix: Add more prominent colors for selected dates and fade out dates in previous/next month * TimePicker: style updates2 * TimePicker: Big refactorings and style changes * Removed use of Popper not sure we need that here? * Made active selected item in the list have the "selected" checkmark * Changed design of popover * Changed design of and implementation of the Custom selection in the dropdown it did not feel like a item you could select like the rest now the list is just a normal list * TimePicker: Refactoring & style changes * TimePicker: use same date format everywhere * TimePicker: Calendar style updates * TimePicker: fixed unit test * fixed unit test * TimeZone: refactoring time zone type * TimePicker: refactoring * TimePicker: finally to UTC to work * TimePicker: better way to handle calendar utc dates * TimePicker: Fixed tooltip issues * Updated snapshot * TimePicker: moved tooltip from DashNavControls into TimePicker
6 years ago
getTimezone(): TimeZone {
return (this.timezone ? this.timezone : contextSrv.user.timezone) as TimeZone;
}
private updateSchema(old: any) {
const 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: any) => {
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: any) => {
return {
name: variable.name,
type: variable.type,
current: _.cloneDeep(variable.current),
filters: _.cloneDeep(variable.filters),
};
});
return !_.isEqual(updated, this.originalTemplating);
}
autoFitPanels(viewHeight: number, kioskMode?: UrlQueryValue) {
const currentGridHeight = Math.max(
...this.panels.map(panel => {
return panel.gridPos.h + panel.gridPos.y;
})
);
const navbarHeight = 55;
const margin = 20;
const submenuHeight = 50;
let visibleHeight = viewHeight - navbarHeight - margin;
// Remove submenu height if visible
if (this.meta.submenuEnabled && !kioskMode) {
visibleHeight -= submenuHeight;
}
// add back navbar height
if (kioskMode && kioskMode !== KIOSK_MODE_TV) {
visibleHeight += navbarHeight;
}
const visibleGridHeight = Math.floor(visibleHeight / (GRID_CELL_HEIGHT + GRID_CELL_VMARGIN));
const scaleFactor = currentGridHeight / visibleGridHeight;
this.panels.forEach((panel, i) => {
panel.gridPos.y = Math.round(panel.gridPos.y / scaleFactor) || 1;
panel.gridPos.h = Math.round(panel.gridPos.h / scaleFactor) || 1;
});
}
templateVariableValueUpdated() {
this.processRepeats();
this.events.emit(CoreEvents.templateVariableValueUpdated);
}
expandParentRowFor(panelId: number) {
for (const panel of this.panels) {
if (panel.collapsed) {
for (const rowPanel of panel.panels) {
if (rowPanel.id === panelId) {
this.toggleRow(panel);
return;
}
}
}
}
}
toggleLegendsForAll() {
const panelsWithLegends = this.panels.filter(panel => {
return panel.legend !== undefined && panel.legend !== null;
});
// determine if more panels are displaying legends or not
const onCount = panelsWithLegends.filter(panel => panel.legend.show).length;
const offCount = panelsWithLegends.length - onCount;
const panelLegendsOn = onCount >= offCount;
for (const panel of panelsWithLegends) {
panel.legend.show = !panelLegendsOn;
panel.render();
}
}
updatePanel = (panelModel: PanelModel) => {
let index = 0;
for (const panel of this.panels) {
if (panel.id === panelModel.id) {
this.panels[index].restoreModel(panelModel.getSaveModel());
this.panels[index].getQueryRunner().pipeDataToSubject(panelModel.getQueryRunner().getLastResult());
break;
}
index++;
}
};
}