12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175 |
- import { cloneDeep, defaults as _defaults, filter, indexOf, isEqual, map, maxBy, pull } from 'lodash';
- import { Subscription } from 'rxjs';
- import {
- AnnotationQuery,
- AppEvent,
- DashboardCursorSync,
- dateTime,
- dateTimeFormat,
- dateTimeFormatTimeAgo,
- DateTimeInput,
- EventBusExtended,
- EventBusSrv,
- PanelModel as IPanelModel,
- TimeRange,
- TimeZone,
- UrlQueryValue,
- } from '@grafana/data';
- import { RefreshEvent, TimeRangeUpdatedEvent } from '@grafana/runtime';
- import { DEFAULT_ANNOTATION_COLOR } from '@grafana/ui';
- import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT, REPEAT_DIR_VERTICAL } from 'app/core/constants';
- import { contextSrv } from 'app/core/services/context_srv';
- import { sortedDeepCloneWithoutNulls } from 'app/core/utils/object';
- import { variableAdapters } from 'app/features/variables/adapters';
- import { onTimeRangeUpdated } from 'app/features/variables/state/actions';
- import { GetVariables, getVariablesByKey } from 'app/features/variables/state/selectors';
- import { CoreEvents, DashboardMeta, KioskMode } from 'app/types';
- import { DashboardPanelsChangedEvent, RenderEvent } from 'app/types/events';
- import { appEvents } from '../../../core/core';
- import { dispatch } from '../../../store/store';
- import {
- VariablesChanged,
- VariablesChangedEvent,
- VariablesChangedInUrl,
- VariablesTimeRangeProcessDone,
- } from '../../variables/types';
- import { isAllVariable } from '../../variables/utils';
- import { getTimeSrv } from '../services/TimeSrv';
- import { mergePanels, PanelMergeInfo } from '../utils/panelMerge';
- import { DashboardMigrator } from './DashboardMigrator';
- import { GridPos, PanelModel } from './PanelModel';
- import { TimeModel } from './TimeModel';
- import { deleteScopeVars, isOnTheSameGridRow } from './utils';
- export interface CloneOptions {
- saveVariables?: boolean;
- saveTimerange?: boolean;
- message?: string;
- }
- export type DashboardLinkType = 'link' | 'dashboards';
- export interface DashboardLink {
- icon: string;
- title: string;
- tooltip: string;
- type: DashboardLinkType;
- url: string;
- asDropdown: boolean;
- tags: any[];
- searchHits?: any[];
- targetBlank: boolean;
- keepTime: boolean;
- includeVars: boolean;
- }
- export class DashboardModel implements TimeModel {
- id: any;
- uid: string;
- title: string;
- autoUpdate: any;
- description: any;
- tags: any;
- style: any;
- timezone: any;
- weekStart: any;
- editable: any;
- graphTooltip: DashboardCursorSync;
- time: any;
- liveNow: boolean;
- private originalTime: any;
- timepicker: any;
- templating: { list: any[] };
- private originalTemplating: any;
- annotations: { list: AnnotationQuery[] };
- refresh: any;
- snapshot: any;
- schemaVersion: number;
- version: number;
- revision: number;
- links: DashboardLink[];
- gnetId: any;
- panels: PanelModel[];
- panelInEdit?: PanelModel;
- panelInView?: PanelModel;
- fiscalYearStartMonth?: number;
- private panelsAffectedByVariableChange: number[] | null;
- private appEventsSubscription: Subscription;
- private lastRefresh: number;
- // ------------------
- // not persisted
- // ------------------
- // repeat process cycles
- declare meta: DashboardMeta;
- events: EventBusExtended;
- static nonPersistedProperties: { [str: string]: boolean } = {
- events: true,
- meta: true,
- panels: true, // needs special handling
- templating: true, // needs special handling
- originalTime: true,
- originalTemplating: true,
- originalLibraryPanels: true,
- panelInEdit: true,
- panelInView: true,
- getVariablesFromState: true,
- formatDate: true,
- appEventsSubscription: true,
- panelsAffectedByVariableChange: true,
- lastRefresh: true,
- };
- constructor(data: any, meta?: DashboardMeta, private getVariablesFromState: GetVariables = getVariablesByKey) {
- if (!data) {
- data = {};
- }
- this.events = new EventBusSrv();
- 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.weekStart = data.weekStart ?? '';
- this.editable = data.editable !== false;
- this.graphTooltip = data.graphTooltip || 0;
- this.time = data.time ?? { from: 'now-6h', to: 'now' };
- this.timepicker = data.timepicker ?? {};
- this.liveNow = Boolean(data.liveNow);
- 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.fiscalYearStartMonth = data.fiscalYearStartMonth ?? 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.ensurePanelsHaveIds();
- this.formatDate = this.formatDate.bind(this);
- this.resetOriginalVariables(true);
- this.resetOriginalTime();
- this.initMeta(meta);
- this.updateSchema(data);
- this.addBuiltInAnnotationQuery();
- this.sortPanelsByGridPos();
- this.panelsAffectedByVariableChange = null;
- this.appEventsSubscription = new Subscription();
- this.lastRefresh = Date.now();
- this.appEventsSubscription.add(appEvents.subscribe(VariablesChanged, this.variablesChangedHandler.bind(this)));
- this.appEventsSubscription.add(
- appEvents.subscribe(VariablesTimeRangeProcessDone, this.variablesTimeRangeProcessDoneHandler.bind(this))
- );
- this.appEventsSubscription.add(
- appEvents.subscribe(VariablesChangedInUrl, this.variablesChangedInUrlHandler.bind(this))
- );
- }
- addBuiltInAnnotationQuery() {
- const found = this.annotations.list.some((item) => item.builtIn === 1);
- if (found) {
- return;
- }
- this.annotations.list.unshift({
- datasource: { uid: '-- Grafana --', type: 'grafana' },
- name: 'Annotations & Alerts',
- type: 'dashboard',
- iconColor: DEFAULT_ANNOTATION_COLOR,
- enable: true,
- hide: true,
- builtIn: 1,
- });
- }
- 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.canDelete = meta.canDelete !== false;
- meta.showSettings = meta.canSave;
- meta.canMakeEditable = meta.canSave && !this.editable;
- meta.hasUnsavedFolderChange = 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): DashboardModel {
- 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]);
- }
- this.updateTemplatingSaveModelClone(copy, defaults);
- if (!defaults.saveTimerange) {
- copy.time = this.originalTime;
- }
- // get panel save models
- copy.panels = this.getPanelSaveModels();
- // sort by keys
- copy = sortedDeepCloneWithoutNulls(copy);
- copy.getVariables = () => copy.templating.list;
- return copy;
- }
- /**
- * This will load a new dashboard, but keep existing panels unchanged
- *
- * This function can be used to implement:
- * 1. potentially faster loading dashboard loading
- * 2. dynamic dashboard behavior
- * 3. "live" dashboard editing
- *
- * @internal and experimental
- */
- updatePanels(panels: IPanelModel[]): PanelMergeInfo {
- const info = mergePanels(this.panels, panels ?? []);
- if (info.changed) {
- this.panels = info.panels ?? [];
- this.sortPanelsByGridPos();
- this.events.publish(new DashboardPanelsChangedEvent());
- }
- return info;
- }
- private getPanelSaveModels() {
- return this.panels
- .filter(
- (panel) =>
- this.isSnapshotTruthy() || !(panel.type === 'add-panel' || panel.repeatPanelId || panel.repeatedByRow)
- )
- .map((panel) => {
- // If we save while editing we should include the panel in edit mode instead of the
- // unmodified source panel
- if (this.panelInEdit && this.panelInEdit.id === panel.id) {
- return this.panelInEdit.getSaveModel();
- }
- return panel.getSaveModel();
- })
- .map((model: any) => {
- if (this.isSnapshotTruthy()) {
- return model;
- }
- // Clear any scopedVars from persisted mode. This cannot be part of getSaveModel as we need to be able to copy
- // panel models with preserved scopedVars, for example when going into edit mode.
- delete model.scopedVars;
- // Clear any repeated panels from collapsed rows
- if (model.type === 'row' && model.panels && model.panels.length > 0) {
- model.panels = model.panels
- .filter((rowPanel: PanelModel) => !rowPanel.repeatPanelId)
- .map((model: PanelModel) => {
- delete model.scopedVars;
- return model;
- });
- }
- return model;
- });
- }
- private updateTemplatingSaveModelClone(
- copy: any,
- defaults: { saveTimerange: boolean; saveVariables: boolean } & CloneOptions
- ) {
- const originalVariables = this.originalTemplating;
- const currentVariables = this.getVariablesFromState(this.uid);
- copy.templating = {
- list: currentVariables.map((variable) =>
- variableAdapters.get(variable.type).getSaveModel(variable, defaults.saveVariables)
- ),
- };
- if (!defaults.saveVariables) {
- for (const current of copy.templating.list) {
- const original = originalVariables.find(
- ({ name, type }: any) => name === current.name && type === current.type
- );
- if (!original) {
- continue;
- }
- if (current.type === 'adhoc') {
- current.filters = original.filters;
- } else {
- current.current = original.current;
- }
- }
- }
- }
- timeRangeUpdated(timeRange: TimeRange) {
- this.events.publish(new TimeRangeUpdatedEvent(timeRange));
- dispatch(onTimeRangeUpdated(this.uid, timeRange));
- }
- startRefresh(event: VariablesChangedEvent = { refreshAll: true, panelIds: [] }) {
- this.events.publish(new RefreshEvent());
- this.lastRefresh = Date.now();
- if (this.panelInEdit && (event.refreshAll || event.panelIds.includes(this.panelInEdit.id))) {
- this.panelInEdit.refresh();
- return;
- }
- for (const panel of this.panels) {
- if (!this.otherPanelInFullscreen(panel) && (event.refreshAll || event.panelIds.includes(panel.id))) {
- panel.refresh();
- }
- }
- }
- render() {
- this.events.publish(new RenderEvent());
- for (const panel of this.panels) {
- panel.render();
- }
- }
- panelInitialized(panel: PanelModel) {
- const lastResult = panel.getQueryRunner().getLastResult();
- if (!this.otherPanelInFullscreen(panel) && !lastResult) {
- panel.refresh();
- }
- }
- otherPanelInFullscreen(panel: PanelModel) {
- return (this.panelInEdit || this.panelInView) && !(panel.isViewing || panel.isEditing);
- }
- initEditPanel(sourcePanel: PanelModel): PanelModel {
- getTimeSrv().pauseAutoRefresh();
- this.panelInEdit = sourcePanel.getEditClone();
- return this.panelInEdit;
- }
- initViewPanel(panel: PanelModel) {
- this.panelInView = panel;
- panel.setIsViewing(true);
- }
- exitViewPanel(panel: PanelModel) {
- this.panelInView = undefined;
- panel.setIsViewing(false);
- this.refreshIfPanelsAffectedByVariableChange();
- }
- exitPanelEditor() {
- this.panelInEdit!.destroy();
- this.panelInEdit = undefined;
- getTimeSrv().resumeAutoRefresh();
- this.refreshIfPanelsAffectedByVariableChange();
- }
- private refreshIfPanelsAffectedByVariableChange() {
- if (!this.panelsAffectedByVariableChange) {
- return;
- }
- this.startRefresh({ panelIds: this.panelsAffectedByVariableChange, refreshAll: false });
- this.panelsAffectedByVariableChange = null;
- }
- private ensurePanelsHaveIds() {
- let nextPanelId = this.getNextPanelId();
- for (const panel of this.panelIterator()) {
- panel.id ??= nextPanelId++;
- }
- }
- private ensureListExist(data: any = {}) {
- data.list ??= [];
- return data;
- }
- getNextPanelId() {
- let max = 0;
- for (const panel of this.panelIterator()) {
- if (panel.id > max) {
- max = panel.id;
- }
- }
- return max + 1;
- }
- *panelIterator() {
- for (const panel of this.panels) {
- yield panel;
- const rowPanels = panel.panels ?? [];
- for (const rowPanel of rowPanels) {
- yield rowPanel;
- }
- }
- }
- 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 | null {
- if (this.panelInEdit && this.panelInEdit.id === id) {
- return this.panelInEdit;
- }
- return this.panels.find((p) => p.id === id) ?? null;
- }
- canEditPanel(panel?: PanelModel | null): boolean | undefined | null {
- return Boolean(this.meta.canEdit && panel && !panel.repeatPanelId && panel.type !== 'row');
- }
- canEditPanelById(id: number): boolean | undefined | null {
- return this.canEditPanel(this.getPanelById(id));
- }
- addPanel(panelData: any) {
- panelData.id = this.getNextPanelId();
- this.panels.unshift(new PanelModel(panelData));
- this.sortPanelsByGridPos();
- this.events.publish(new DashboardPanelsChangedEvent());
- }
- 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;
- }
- });
- }
- clearUnsavedChanges() {
- for (const panel of this.panels) {
- panel.configRev = 0;
- }
- }
- hasUnsavedChanges() {
- const changedPanel = this.panels.find((p) => p.hasChanged);
- return Boolean(changedPanel);
- }
- cleanUpRepeats() {
- if (this.isSnapshotTruthy() || !this.hasVariables()) {
- return;
- }
- // cleanup scopedVars
- deleteScopeVars(this.panels);
- const panelsToRemove = this.panels.filter((p) => (!p.repeat || p.repeatedByRow) && p.repeatPanelId);
- // remove panels
- pull(this.panels, ...panelsToRemove);
- panelsToRemove.map((p) => p.destroy());
- this.sortPanelsByGridPos();
- }
- processRepeats() {
- if (this.isSnapshotTruthy() || !this.hasVariables()) {
- return;
- }
- this.cleanUpRepeats();
- 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.publish(new DashboardPanelsChangedEvent());
- }
- cleanUpRowRepeats(rowPanels: PanelModel[]) {
- const panelIds = rowPanels.map((row) => row.id);
- // Remove repeated panels whose parent is in this row as these will be recreated later in processRowRepeats
- const panelsToRemove = rowPanels.filter((p) => !p.repeat && p.repeatPanelId && panelIds.includes(p.repeatPanelId));
- pull(rowPanels, ...panelsToRemove);
- pull(this.panels, ...panelsToRemove);
- }
- processRowRepeats(row: PanelModel) {
- if (this.isSnapshotTruthy() || !this.hasVariables()) {
- return;
- }
- let rowPanels = row.panels ?? [];
- if (!row.collapsed) {
- const rowPanelIndex = this.panels.findIndex((p) => p.id === row.id);
- rowPanels = this.getRowPanels(rowPanelIndex);
- }
- this.cleanUpRowRepeats(rowPanels);
- for (const panel of rowPanels) {
- if (panel.repeat) {
- const panelIndex = this.panels.findIndex((p) => 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 m = sourcePanel.getSaveModel();
- m.id = this.getNextPanelId();
- const clone = new PanelModel(m);
- // insert after source panel + value index
- this.panels.splice(sourcePanelIndex + valueIndex, 0, clone);
- clone.repeatPanelId = sourcePanel.id;
- clone.repeat = undefined;
- if (this.panelInView?.id === clone.id) {
- clone.setIsViewing(true);
- this.panelInView = clone;
- }
- 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 = rowPanels.map((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) {
- const variable = this.getPanelRepeatVariable(panel);
- 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[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 (const curPanel of this.panels.slice(panelBelowIndex)) {
- if (isOnTheSameGridRow(panel, curPanel)) {
- continue;
- }
- curPanel.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[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
- for (const rowPanel of rowPanels) {
- 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;
- rowPanels.forEach((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 if we inserted more than 1 repeated row panel
- if (selectedOptions.length > 1) {
- for (const panel of this.panels.slice(panelBelowIndex)) {
- panel.gridPos.y += yPos;
- }
- }
- }
- }
- updateRepeatedPanelIds(panel: PanelModel, repeatedByRow?: boolean) {
- panel.repeatPanelId = panel.id;
- panel.id = this.getNextPanelId();
- if (repeatedByRow) {
- panel.repeatedByRow = true;
- } else {
- panel.repeat = undefined;
- }
- return panel;
- }
- getSelectedVariableOptions(variable: any) {
- let selectedOptions: any[];
- if (isAllVariable(variable)) {
- 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) => pos.y + pos.h);
- return maxPos!.y + maxPos!.h - rowYPos;
- }
- removePanel(panel: PanelModel) {
- this.panels = this.panels.filter((item) => item !== panel);
- this.events.publish(new DashboardPanelsChangedEvent());
- }
- removeRow(row: PanelModel, removePanels: boolean) {
- const needToggle = (!removePanels && row.collapsed) || (removePanels && !row.collapsed);
- if (needToggle) {
- this.toggleRow(row);
- }
- this.removePanel(row);
- }
- expandRows() {
- const collapsedRows = this.panels.filter((p) => p.type === 'row' && p.collapsed);
- for (const row of collapsedRows) {
- this.toggleRow(row);
- }
- }
- collapseRows() {
- const collapsedRows = this.panels.filter((p) => p.type === 'row' && !p.collapsed);
- for (const row of collapsedRows) {
- this.toggleRow(row);
- }
- }
- isSubMenuVisible() {
- return (
- this.links.length > 0 ||
- this.getVariables().some((variable) => variable.hide !== 2) ||
- this.annotations.list.some((annotation) => !annotation.hide)
- );
- }
- getPanelInfoById(panelId: number) {
- const panelIndex = this.panels.findIndex((p) => p.id === panelId);
- return panelIndex >= 0 ? { panel: this.panels[panelIndex], index: panelIndex } : 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) {
- return dateTimeFormat(date, {
- format,
- timeZone: this.getTimezone(),
- });
- }
- destroy() {
- this.appEventsSubscription.unsubscribe();
- this.events.removeAllListeners();
- for (const panel of this.panels) {
- panel.destroy();
- }
- }
- toggleRow(row: PanelModel) {
- const rowIndex = indexOf(this.panels, row);
- if (!row.collapsed) {
- const rowPanels = this.getRowPanels(rowIndex);
- // remove panels
- pull(this.panels, ...rowPanels);
- // save panel models inside row panel
- row.panels = rowPanels.map((panel: PanelModel) => panel.getSaveModel());
- row.collapsed = true;
- if (rowPanels.some((panel) => panel.hasChanged)) {
- row.configRev++;
- }
- // emit change event
- this.events.publish(new DashboardPanelsChangedEvent());
- return;
- }
- row.collapsed = false;
- const rowPanels = row.panels ?? [];
- const hasRepeat = rowPanels.some((p: PanelModel) => p.repeat);
- if (rowPanels.length > 0) {
- // Use first panel to figure out if it was moved or pushed
- // If the panel doesn't have gridPos.y, use the row gridPos.y instead.
- // This can happen for some generated dashboards.
- const firstPanelYPos = rowPanels[0].gridPos.y ?? row.gridPos.y;
- const yDiff = firstPanelYPos - (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 (const panel of rowPanels) {
- // set the y gridPos if it wasn't already set
- panel.gridPos.y ?? (panel.gridPos.y = row.gridPos.y); // (Safari 13.1 lacks ??= support)
- // make sure y is adjusted (in case row moved while collapsed)
- 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 (const panel of this.panels.slice(insertPos)) {
- panel.gridPos.y += pushDownAmount;
- }
- row.panels = [];
- if (hasRepeat) {
- this.processRowRepeats(row);
- }
- }
- // sort panels
- this.sortPanelsByGridPos();
- // emit change event
- this.events.publish(new DashboardPanelsChangedEvent());
- }
- /**
- * Will return all panels after rowIndex until it encounters another row
- */
- getRowPanels(rowIndex: number): PanelModel[] {
- const panelsBelowRow = this.panels.slice(rowIndex + 1);
- const nextRowIndex = panelsBelowRow.findIndex((p) => p.type === 'row');
- // Take all panels up to next row, or all panels if there are no other rows
- const rowPanels = panelsBelowRow.slice(0, nextRowIndex >= 0 ? nextRowIndex : this.panels.length);
- return rowPanels;
- }
- /** @deprecated */
- on<T>(event: AppEvent<T>, callback: (payload?: T) => void) {
- console.log('DashboardModel.on is deprecated use events.subscribe');
- this.events.on(event, callback);
- }
- /** @deprecated */
- off<T>(event: AppEvent<T>, callback: (payload?: T) => void) {
- console.log('DashboardModel.off is deprecated');
- 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) {
- return dateTimeFormatTimeAgo(date, {
- timeZone: this.getTimezone(),
- });
- }
- isSnapshot() {
- return this.snapshot !== undefined;
- }
- 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() {
- const { time, originalTime } = this;
- // Compare moment values vs strings values
- return !(
- isEqual(time, originalTime) ||
- (isEqual(dateTime(time?.from), dateTime(originalTime?.from)) &&
- isEqual(dateTime(time?.to), dateTime(originalTime?.to)))
- );
- }
- resetOriginalVariables(initial = false) {
- if (initial) {
- this.originalTemplating = this.cloneVariablesFrom(this.templating.list);
- return;
- }
- this.originalTemplating = this.cloneVariablesFrom(this.getVariablesFromState(this.uid));
- }
- hasVariableValuesChanged() {
- return this.hasVariablesChanged(this.originalTemplating, this.getVariablesFromState(this.uid));
- }
- autoFitPanels(viewHeight: number, kioskMode?: UrlQueryValue) {
- const currentGridHeight = Math.max(...this.panels.map((panel) => 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 !== KioskMode.TV) {
- visibleHeight += navbarHeight;
- }
- const visibleGridHeight = Math.floor(visibleHeight / (GRID_CELL_HEIGHT + GRID_CELL_VMARGIN));
- const scaleFactor = currentGridHeight / visibleGridHeight;
- for (const panel of this.panels) {
- 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);
- }
- getPanelByUrlId(panelUrlId: string) {
- const panelId = parseInt(panelUrlId ?? '0', 10);
- // First try to find it in a collapsed row and exand it
- const collapsedPanels = this.panels.filter((p) => p.collapsed);
- for (const panel of collapsedPanels) {
- const hasPanel = panel.panels?.some((rp: any) => rp.id === panelId);
- hasPanel && this.toggleRow(panel);
- }
- return this.getPanelById(panelId);
- }
- toggleLegendsForAll() {
- const panelsWithLegends = this.panels.filter(isPanelWithLegend);
- // 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();
- }
- }
- getVariables() {
- return this.getVariablesFromState(this.uid);
- }
- canEditAnnotations(dashboardId: number) {
- let canEdit = true;
- // if RBAC is enabled there are additional conditions to check
- if (contextSrv.accessControlEnabled()) {
- if (dashboardId === 0) {
- canEdit = !!this.meta.annotationsPermissions?.organization.canEdit;
- } else {
- canEdit = !!this.meta.annotationsPermissions?.dashboard.canEdit;
- }
- }
- return this.canEditDashboard() && canEdit;
- }
- canAddAnnotations() {
- // If RBAC is enabled there are additional conditions to check
- const canAdd = !contextSrv.accessControlEnabled() || this.meta.annotationsPermissions?.dashboard.canAdd;
- return this.canEditDashboard() && canAdd;
- }
- canEditDashboard() {
- return this.meta.canEdit || this.meta.canMakeEditable;
- }
- shouldUpdateDashboardPanelFromJSON(updatedPanel: PanelModel, panel: PanelModel) {
- const shouldUpdateGridPositionLayout = !isEqual(updatedPanel?.gridPos, panel?.gridPos);
- if (shouldUpdateGridPositionLayout) {
- this.events.publish(new DashboardPanelsChangedEvent());
- }
- }
- private getPanelRepeatVariable(panel: PanelModel) {
- return this.getVariablesFromState(this.uid).find((variable) => variable.name === panel.repeat);
- }
- private isSnapshotTruthy() {
- return this.snapshot;
- }
- private hasVariables() {
- return this.getVariablesFromState(this.uid).length > 0;
- }
- private hasVariablesChanged(originalVariables: any[], currentVariables: any[]): boolean {
- if (originalVariables.length !== currentVariables.length) {
- return false;
- }
- const updated = currentVariables.map((variable: any) => ({
- name: variable.name,
- type: variable.type,
- current: cloneDeep(variable.current),
- filters: cloneDeep(variable.filters),
- }));
- return !isEqual(updated, originalVariables);
- }
- private cloneVariablesFrom(variables: any[]): any[] {
- return variables.map((variable) => ({
- name: variable.name,
- type: variable.type,
- current: cloneDeep(variable.current),
- filters: cloneDeep(variable.filters),
- }));
- }
- private variablesTimeRangeProcessDoneHandler(event: VariablesTimeRangeProcessDone) {
- const processRepeats = event.payload.variableIds.length > 0;
- this.variablesChangedHandler(new VariablesChanged({ panelIds: [], refreshAll: true }), processRepeats);
- }
- private variablesChangedHandler(event: VariablesChanged, processRepeats = true) {
- if (processRepeats) {
- this.processRepeats();
- }
- if (event.payload.refreshAll || getTimeSrv().isRefreshOutsideThreshold(this.lastRefresh)) {
- this.startRefresh({ refreshAll: true, panelIds: [] });
- return;
- }
- if (this.panelInEdit || this.panelInView) {
- this.panelsAffectedByVariableChange = event.payload.panelIds.filter(
- (id) => id !== (this.panelInEdit?.id ?? this.panelInView?.id)
- );
- }
- this.startRefresh(event.payload);
- }
- private variablesChangedInUrlHandler(event: VariablesChangedInUrl) {
- this.templateVariableValueUpdated();
- this.startRefresh(event.payload);
- }
- }
- function isPanelWithLegend(panel: PanelModel): panel is PanelModel & Pick<Required<PanelModel>, 'legend'> {
- return Boolean(panel.legend);
- }
|