DashboardModel.ts 34 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175
  1. import { cloneDeep, defaults as _defaults, filter, indexOf, isEqual, map, maxBy, pull } from 'lodash';
  2. import { Subscription } from 'rxjs';
  3. import {
  4. AnnotationQuery,
  5. AppEvent,
  6. DashboardCursorSync,
  7. dateTime,
  8. dateTimeFormat,
  9. dateTimeFormatTimeAgo,
  10. DateTimeInput,
  11. EventBusExtended,
  12. EventBusSrv,
  13. PanelModel as IPanelModel,
  14. TimeRange,
  15. TimeZone,
  16. UrlQueryValue,
  17. } from '@grafana/data';
  18. import { RefreshEvent, TimeRangeUpdatedEvent } from '@grafana/runtime';
  19. import { DEFAULT_ANNOTATION_COLOR } from '@grafana/ui';
  20. import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT, REPEAT_DIR_VERTICAL } from 'app/core/constants';
  21. import { contextSrv } from 'app/core/services/context_srv';
  22. import { sortedDeepCloneWithoutNulls } from 'app/core/utils/object';
  23. import { variableAdapters } from 'app/features/variables/adapters';
  24. import { onTimeRangeUpdated } from 'app/features/variables/state/actions';
  25. import { GetVariables, getVariablesByKey } from 'app/features/variables/state/selectors';
  26. import { CoreEvents, DashboardMeta, KioskMode } from 'app/types';
  27. import { DashboardPanelsChangedEvent, RenderEvent } from 'app/types/events';
  28. import { appEvents } from '../../../core/core';
  29. import { dispatch } from '../../../store/store';
  30. import {
  31. VariablesChanged,
  32. VariablesChangedEvent,
  33. VariablesChangedInUrl,
  34. VariablesTimeRangeProcessDone,
  35. } from '../../variables/types';
  36. import { isAllVariable } from '../../variables/utils';
  37. import { getTimeSrv } from '../services/TimeSrv';
  38. import { mergePanels, PanelMergeInfo } from '../utils/panelMerge';
  39. import { DashboardMigrator } from './DashboardMigrator';
  40. import { GridPos, PanelModel } from './PanelModel';
  41. import { TimeModel } from './TimeModel';
  42. import { deleteScopeVars, isOnTheSameGridRow } from './utils';
  43. export interface CloneOptions {
  44. saveVariables?: boolean;
  45. saveTimerange?: boolean;
  46. message?: string;
  47. }
  48. export type DashboardLinkType = 'link' | 'dashboards';
  49. export interface DashboardLink {
  50. icon: string;
  51. title: string;
  52. tooltip: string;
  53. type: DashboardLinkType;
  54. url: string;
  55. asDropdown: boolean;
  56. tags: any[];
  57. searchHits?: any[];
  58. targetBlank: boolean;
  59. keepTime: boolean;
  60. includeVars: boolean;
  61. }
  62. export class DashboardModel implements TimeModel {
  63. id: any;
  64. uid: string;
  65. title: string;
  66. autoUpdate: any;
  67. description: any;
  68. tags: any;
  69. style: any;
  70. timezone: any;
  71. weekStart: any;
  72. editable: any;
  73. graphTooltip: DashboardCursorSync;
  74. time: any;
  75. liveNow: boolean;
  76. private originalTime: any;
  77. timepicker: any;
  78. templating: { list: any[] };
  79. private originalTemplating: any;
  80. annotations: { list: AnnotationQuery[] };
  81. refresh: any;
  82. snapshot: any;
  83. schemaVersion: number;
  84. version: number;
  85. revision: number;
  86. links: DashboardLink[];
  87. gnetId: any;
  88. panels: PanelModel[];
  89. panelInEdit?: PanelModel;
  90. panelInView?: PanelModel;
  91. fiscalYearStartMonth?: number;
  92. private panelsAffectedByVariableChange: number[] | null;
  93. private appEventsSubscription: Subscription;
  94. private lastRefresh: number;
  95. // ------------------
  96. // not persisted
  97. // ------------------
  98. // repeat process cycles
  99. declare meta: DashboardMeta;
  100. events: EventBusExtended;
  101. static nonPersistedProperties: { [str: string]: boolean } = {
  102. events: true,
  103. meta: true,
  104. panels: true, // needs special handling
  105. templating: true, // needs special handling
  106. originalTime: true,
  107. originalTemplating: true,
  108. originalLibraryPanels: true,
  109. panelInEdit: true,
  110. panelInView: true,
  111. getVariablesFromState: true,
  112. formatDate: true,
  113. appEventsSubscription: true,
  114. panelsAffectedByVariableChange: true,
  115. lastRefresh: true,
  116. };
  117. constructor(data: any, meta?: DashboardMeta, private getVariablesFromState: GetVariables = getVariablesByKey) {
  118. if (!data) {
  119. data = {};
  120. }
  121. this.events = new EventBusSrv();
  122. this.id = data.id || null;
  123. this.uid = data.uid || null;
  124. this.revision = data.revision;
  125. this.title = data.title ?? 'No Title';
  126. this.autoUpdate = data.autoUpdate;
  127. this.description = data.description;
  128. this.tags = data.tags ?? [];
  129. this.style = data.style ?? 'dark';
  130. this.timezone = data.timezone ?? '';
  131. this.weekStart = data.weekStart ?? '';
  132. this.editable = data.editable !== false;
  133. this.graphTooltip = data.graphTooltip || 0;
  134. this.time = data.time ?? { from: 'now-6h', to: 'now' };
  135. this.timepicker = data.timepicker ?? {};
  136. this.liveNow = Boolean(data.liveNow);
  137. this.templating = this.ensureListExist(data.templating);
  138. this.annotations = this.ensureListExist(data.annotations);
  139. this.refresh = data.refresh;
  140. this.snapshot = data.snapshot;
  141. this.schemaVersion = data.schemaVersion ?? 0;
  142. this.fiscalYearStartMonth = data.fiscalYearStartMonth ?? 0;
  143. this.version = data.version ?? 0;
  144. this.links = data.links ?? [];
  145. this.gnetId = data.gnetId || null;
  146. this.panels = map(data.panels ?? [], (panelData: any) => new PanelModel(panelData));
  147. this.ensurePanelsHaveIds();
  148. this.formatDate = this.formatDate.bind(this);
  149. this.resetOriginalVariables(true);
  150. this.resetOriginalTime();
  151. this.initMeta(meta);
  152. this.updateSchema(data);
  153. this.addBuiltInAnnotationQuery();
  154. this.sortPanelsByGridPos();
  155. this.panelsAffectedByVariableChange = null;
  156. this.appEventsSubscription = new Subscription();
  157. this.lastRefresh = Date.now();
  158. this.appEventsSubscription.add(appEvents.subscribe(VariablesChanged, this.variablesChangedHandler.bind(this)));
  159. this.appEventsSubscription.add(
  160. appEvents.subscribe(VariablesTimeRangeProcessDone, this.variablesTimeRangeProcessDoneHandler.bind(this))
  161. );
  162. this.appEventsSubscription.add(
  163. appEvents.subscribe(VariablesChangedInUrl, this.variablesChangedInUrlHandler.bind(this))
  164. );
  165. }
  166. addBuiltInAnnotationQuery() {
  167. const found = this.annotations.list.some((item) => item.builtIn === 1);
  168. if (found) {
  169. return;
  170. }
  171. this.annotations.list.unshift({
  172. datasource: { uid: '-- Grafana --', type: 'grafana' },
  173. name: 'Annotations & Alerts',
  174. type: 'dashboard',
  175. iconColor: DEFAULT_ANNOTATION_COLOR,
  176. enable: true,
  177. hide: true,
  178. builtIn: 1,
  179. });
  180. }
  181. private initMeta(meta?: DashboardMeta) {
  182. meta = meta || {};
  183. meta.canShare = meta.canShare !== false;
  184. meta.canSave = meta.canSave !== false;
  185. meta.canStar = meta.canStar !== false;
  186. meta.canEdit = meta.canEdit !== false;
  187. meta.canDelete = meta.canDelete !== false;
  188. meta.showSettings = meta.canSave;
  189. meta.canMakeEditable = meta.canSave && !this.editable;
  190. meta.hasUnsavedFolderChange = false;
  191. if (!this.editable) {
  192. meta.canEdit = false;
  193. meta.canDelete = false;
  194. meta.canSave = false;
  195. }
  196. this.meta = meta;
  197. }
  198. // cleans meta data and other non persistent state
  199. getSaveModelClone(options?: CloneOptions): DashboardModel {
  200. const defaults = _defaults(options || {}, {
  201. saveVariables: true,
  202. saveTimerange: true,
  203. });
  204. // make clone
  205. let copy: any = {};
  206. for (const property in this) {
  207. if (DashboardModel.nonPersistedProperties[property] || !this.hasOwnProperty(property)) {
  208. continue;
  209. }
  210. copy[property] = cloneDeep(this[property]);
  211. }
  212. this.updateTemplatingSaveModelClone(copy, defaults);
  213. if (!defaults.saveTimerange) {
  214. copy.time = this.originalTime;
  215. }
  216. // get panel save models
  217. copy.panels = this.getPanelSaveModels();
  218. // sort by keys
  219. copy = sortedDeepCloneWithoutNulls(copy);
  220. copy.getVariables = () => copy.templating.list;
  221. return copy;
  222. }
  223. /**
  224. * This will load a new dashboard, but keep existing panels unchanged
  225. *
  226. * This function can be used to implement:
  227. * 1. potentially faster loading dashboard loading
  228. * 2. dynamic dashboard behavior
  229. * 3. "live" dashboard editing
  230. *
  231. * @internal and experimental
  232. */
  233. updatePanels(panels: IPanelModel[]): PanelMergeInfo {
  234. const info = mergePanels(this.panels, panels ?? []);
  235. if (info.changed) {
  236. this.panels = info.panels ?? [];
  237. this.sortPanelsByGridPos();
  238. this.events.publish(new DashboardPanelsChangedEvent());
  239. }
  240. return info;
  241. }
  242. private getPanelSaveModels() {
  243. return this.panels
  244. .filter(
  245. (panel) =>
  246. this.isSnapshotTruthy() || !(panel.type === 'add-panel' || panel.repeatPanelId || panel.repeatedByRow)
  247. )
  248. .map((panel) => {
  249. // If we save while editing we should include the panel in edit mode instead of the
  250. // unmodified source panel
  251. if (this.panelInEdit && this.panelInEdit.id === panel.id) {
  252. return this.panelInEdit.getSaveModel();
  253. }
  254. return panel.getSaveModel();
  255. })
  256. .map((model: any) => {
  257. if (this.isSnapshotTruthy()) {
  258. return model;
  259. }
  260. // Clear any scopedVars from persisted mode. This cannot be part of getSaveModel as we need to be able to copy
  261. // panel models with preserved scopedVars, for example when going into edit mode.
  262. delete model.scopedVars;
  263. // Clear any repeated panels from collapsed rows
  264. if (model.type === 'row' && model.panels && model.panels.length > 0) {
  265. model.panels = model.panels
  266. .filter((rowPanel: PanelModel) => !rowPanel.repeatPanelId)
  267. .map((model: PanelModel) => {
  268. delete model.scopedVars;
  269. return model;
  270. });
  271. }
  272. return model;
  273. });
  274. }
  275. private updateTemplatingSaveModelClone(
  276. copy: any,
  277. defaults: { saveTimerange: boolean; saveVariables: boolean } & CloneOptions
  278. ) {
  279. const originalVariables = this.originalTemplating;
  280. const currentVariables = this.getVariablesFromState(this.uid);
  281. copy.templating = {
  282. list: currentVariables.map((variable) =>
  283. variableAdapters.get(variable.type).getSaveModel(variable, defaults.saveVariables)
  284. ),
  285. };
  286. if (!defaults.saveVariables) {
  287. for (const current of copy.templating.list) {
  288. const original = originalVariables.find(
  289. ({ name, type }: any) => name === current.name && type === current.type
  290. );
  291. if (!original) {
  292. continue;
  293. }
  294. if (current.type === 'adhoc') {
  295. current.filters = original.filters;
  296. } else {
  297. current.current = original.current;
  298. }
  299. }
  300. }
  301. }
  302. timeRangeUpdated(timeRange: TimeRange) {
  303. this.events.publish(new TimeRangeUpdatedEvent(timeRange));
  304. dispatch(onTimeRangeUpdated(this.uid, timeRange));
  305. }
  306. startRefresh(event: VariablesChangedEvent = { refreshAll: true, panelIds: [] }) {
  307. this.events.publish(new RefreshEvent());
  308. this.lastRefresh = Date.now();
  309. if (this.panelInEdit && (event.refreshAll || event.panelIds.includes(this.panelInEdit.id))) {
  310. this.panelInEdit.refresh();
  311. return;
  312. }
  313. for (const panel of this.panels) {
  314. if (!this.otherPanelInFullscreen(panel) && (event.refreshAll || event.panelIds.includes(panel.id))) {
  315. panel.refresh();
  316. }
  317. }
  318. }
  319. render() {
  320. this.events.publish(new RenderEvent());
  321. for (const panel of this.panels) {
  322. panel.render();
  323. }
  324. }
  325. panelInitialized(panel: PanelModel) {
  326. const lastResult = panel.getQueryRunner().getLastResult();
  327. if (!this.otherPanelInFullscreen(panel) && !lastResult) {
  328. panel.refresh();
  329. }
  330. }
  331. otherPanelInFullscreen(panel: PanelModel) {
  332. return (this.panelInEdit || this.panelInView) && !(panel.isViewing || panel.isEditing);
  333. }
  334. initEditPanel(sourcePanel: PanelModel): PanelModel {
  335. getTimeSrv().pauseAutoRefresh();
  336. this.panelInEdit = sourcePanel.getEditClone();
  337. return this.panelInEdit;
  338. }
  339. initViewPanel(panel: PanelModel) {
  340. this.panelInView = panel;
  341. panel.setIsViewing(true);
  342. }
  343. exitViewPanel(panel: PanelModel) {
  344. this.panelInView = undefined;
  345. panel.setIsViewing(false);
  346. this.refreshIfPanelsAffectedByVariableChange();
  347. }
  348. exitPanelEditor() {
  349. this.panelInEdit!.destroy();
  350. this.panelInEdit = undefined;
  351. getTimeSrv().resumeAutoRefresh();
  352. this.refreshIfPanelsAffectedByVariableChange();
  353. }
  354. private refreshIfPanelsAffectedByVariableChange() {
  355. if (!this.panelsAffectedByVariableChange) {
  356. return;
  357. }
  358. this.startRefresh({ panelIds: this.panelsAffectedByVariableChange, refreshAll: false });
  359. this.panelsAffectedByVariableChange = null;
  360. }
  361. private ensurePanelsHaveIds() {
  362. let nextPanelId = this.getNextPanelId();
  363. for (const panel of this.panelIterator()) {
  364. panel.id ??= nextPanelId++;
  365. }
  366. }
  367. private ensureListExist(data: any = {}) {
  368. data.list ??= [];
  369. return data;
  370. }
  371. getNextPanelId() {
  372. let max = 0;
  373. for (const panel of this.panelIterator()) {
  374. if (panel.id > max) {
  375. max = panel.id;
  376. }
  377. }
  378. return max + 1;
  379. }
  380. *panelIterator() {
  381. for (const panel of this.panels) {
  382. yield panel;
  383. const rowPanels = panel.panels ?? [];
  384. for (const rowPanel of rowPanels) {
  385. yield rowPanel;
  386. }
  387. }
  388. }
  389. forEachPanel(callback: (panel: PanelModel, index: number) => void) {
  390. for (let i = 0; i < this.panels.length; i++) {
  391. callback(this.panels[i], i);
  392. }
  393. }
  394. getPanelById(id: number): PanelModel | null {
  395. if (this.panelInEdit && this.panelInEdit.id === id) {
  396. return this.panelInEdit;
  397. }
  398. return this.panels.find((p) => p.id === id) ?? null;
  399. }
  400. canEditPanel(panel?: PanelModel | null): boolean | undefined | null {
  401. return Boolean(this.meta.canEdit && panel && !panel.repeatPanelId && panel.type !== 'row');
  402. }
  403. canEditPanelById(id: number): boolean | undefined | null {
  404. return this.canEditPanel(this.getPanelById(id));
  405. }
  406. addPanel(panelData: any) {
  407. panelData.id = this.getNextPanelId();
  408. this.panels.unshift(new PanelModel(panelData));
  409. this.sortPanelsByGridPos();
  410. this.events.publish(new DashboardPanelsChangedEvent());
  411. }
  412. sortPanelsByGridPos() {
  413. this.panels.sort((panelA, panelB) => {
  414. if (panelA.gridPos.y === panelB.gridPos.y) {
  415. return panelA.gridPos.x - panelB.gridPos.x;
  416. } else {
  417. return panelA.gridPos.y - panelB.gridPos.y;
  418. }
  419. });
  420. }
  421. clearUnsavedChanges() {
  422. for (const panel of this.panels) {
  423. panel.configRev = 0;
  424. }
  425. }
  426. hasUnsavedChanges() {
  427. const changedPanel = this.panels.find((p) => p.hasChanged);
  428. return Boolean(changedPanel);
  429. }
  430. cleanUpRepeats() {
  431. if (this.isSnapshotTruthy() || !this.hasVariables()) {
  432. return;
  433. }
  434. // cleanup scopedVars
  435. deleteScopeVars(this.panels);
  436. const panelsToRemove = this.panels.filter((p) => (!p.repeat || p.repeatedByRow) && p.repeatPanelId);
  437. // remove panels
  438. pull(this.panels, ...panelsToRemove);
  439. panelsToRemove.map((p) => p.destroy());
  440. this.sortPanelsByGridPos();
  441. }
  442. processRepeats() {
  443. if (this.isSnapshotTruthy() || !this.hasVariables()) {
  444. return;
  445. }
  446. this.cleanUpRepeats();
  447. for (let i = 0; i < this.panels.length; i++) {
  448. const panel = this.panels[i];
  449. if (panel.repeat) {
  450. this.repeatPanel(panel, i);
  451. }
  452. }
  453. this.sortPanelsByGridPos();
  454. this.events.publish(new DashboardPanelsChangedEvent());
  455. }
  456. cleanUpRowRepeats(rowPanels: PanelModel[]) {
  457. const panelIds = rowPanels.map((row) => row.id);
  458. // Remove repeated panels whose parent is in this row as these will be recreated later in processRowRepeats
  459. const panelsToRemove = rowPanels.filter((p) => !p.repeat && p.repeatPanelId && panelIds.includes(p.repeatPanelId));
  460. pull(rowPanels, ...panelsToRemove);
  461. pull(this.panels, ...panelsToRemove);
  462. }
  463. processRowRepeats(row: PanelModel) {
  464. if (this.isSnapshotTruthy() || !this.hasVariables()) {
  465. return;
  466. }
  467. let rowPanels = row.panels ?? [];
  468. if (!row.collapsed) {
  469. const rowPanelIndex = this.panels.findIndex((p) => p.id === row.id);
  470. rowPanels = this.getRowPanels(rowPanelIndex);
  471. }
  472. this.cleanUpRowRepeats(rowPanels);
  473. for (const panel of rowPanels) {
  474. if (panel.repeat) {
  475. const panelIndex = this.panels.findIndex((p) => p.id === panel.id);
  476. this.repeatPanel(panel, panelIndex);
  477. }
  478. }
  479. }
  480. getPanelRepeatClone(sourcePanel: PanelModel, valueIndex: number, sourcePanelIndex: number) {
  481. // if first clone return source
  482. if (valueIndex === 0) {
  483. return sourcePanel;
  484. }
  485. const m = sourcePanel.getSaveModel();
  486. m.id = this.getNextPanelId();
  487. const clone = new PanelModel(m);
  488. // insert after source panel + value index
  489. this.panels.splice(sourcePanelIndex + valueIndex, 0, clone);
  490. clone.repeatPanelId = sourcePanel.id;
  491. clone.repeat = undefined;
  492. if (this.panelInView?.id === clone.id) {
  493. clone.setIsViewing(true);
  494. this.panelInView = clone;
  495. }
  496. return clone;
  497. }
  498. getRowRepeatClone(sourceRowPanel: PanelModel, valueIndex: number, sourcePanelIndex: number) {
  499. // if first clone return source
  500. if (valueIndex === 0) {
  501. if (!sourceRowPanel.collapsed) {
  502. const rowPanels = this.getRowPanels(sourcePanelIndex);
  503. sourceRowPanel.panels = rowPanels;
  504. }
  505. return sourceRowPanel;
  506. }
  507. const clone = new PanelModel(sourceRowPanel.getSaveModel());
  508. // for row clones we need to figure out panels under row to clone and where to insert clone
  509. let rowPanels: PanelModel[], insertPos: number;
  510. if (sourceRowPanel.collapsed) {
  511. rowPanels = cloneDeep(sourceRowPanel.panels) ?? [];
  512. clone.panels = rowPanels;
  513. // insert copied row after preceding row
  514. insertPos = sourcePanelIndex + valueIndex;
  515. } else {
  516. rowPanels = this.getRowPanels(sourcePanelIndex);
  517. clone.panels = rowPanels.map((panel) => panel.getSaveModel());
  518. // insert copied row after preceding row's panels
  519. insertPos = sourcePanelIndex + (rowPanels.length + 1) * valueIndex;
  520. }
  521. this.panels.splice(insertPos, 0, clone);
  522. this.updateRepeatedPanelIds(clone);
  523. return clone;
  524. }
  525. repeatPanel(panel: PanelModel, panelIndex: number) {
  526. const variable = this.getPanelRepeatVariable(panel);
  527. if (!variable) {
  528. return;
  529. }
  530. if (panel.type === 'row') {
  531. this.repeatRow(panel, panelIndex, variable);
  532. return;
  533. }
  534. const selectedOptions = this.getSelectedVariableOptions(variable);
  535. const maxPerRow = panel.maxPerRow || 4;
  536. let xPos = 0;
  537. let yPos = panel.gridPos.y;
  538. for (let index = 0; index < selectedOptions.length; index++) {
  539. const option = selectedOptions[index];
  540. let copy;
  541. copy = this.getPanelRepeatClone(panel, index, panelIndex);
  542. copy.scopedVars ??= {};
  543. copy.scopedVars[variable.name] = option;
  544. if (panel.repeatDirection === REPEAT_DIR_VERTICAL) {
  545. if (index > 0) {
  546. yPos += copy.gridPos.h;
  547. }
  548. copy.gridPos.y = yPos;
  549. } else {
  550. // set width based on how many are selected
  551. // assumed the repeated panels should take up full row width
  552. copy.gridPos.w = Math.max(GRID_COLUMN_COUNT / selectedOptions.length, GRID_COLUMN_COUNT / maxPerRow);
  553. copy.gridPos.x = xPos;
  554. copy.gridPos.y = yPos;
  555. xPos += copy.gridPos.w;
  556. // handle overflow by pushing down one row
  557. if (xPos + copy.gridPos.w > GRID_COLUMN_COUNT) {
  558. xPos = 0;
  559. yPos += copy.gridPos.h;
  560. }
  561. }
  562. }
  563. // Update gridPos for panels below
  564. const yOffset = yPos - panel.gridPos.y;
  565. if (yOffset > 0) {
  566. const panelBelowIndex = panelIndex + selectedOptions.length;
  567. for (const curPanel of this.panels.slice(panelBelowIndex)) {
  568. if (isOnTheSameGridRow(panel, curPanel)) {
  569. continue;
  570. }
  571. curPanel.gridPos.y += yOffset;
  572. }
  573. }
  574. }
  575. repeatRow(panel: PanelModel, panelIndex: number, variable: any) {
  576. const selectedOptions = this.getSelectedVariableOptions(variable);
  577. let yPos = panel.gridPos.y;
  578. function setScopedVars(panel: PanelModel, variableOption: any) {
  579. panel.scopedVars ??= {};
  580. panel.scopedVars[variable.name] = variableOption;
  581. }
  582. for (let optionIndex = 0; optionIndex < selectedOptions.length; optionIndex++) {
  583. const option = selectedOptions[optionIndex];
  584. const rowCopy = this.getRowRepeatClone(panel, optionIndex, panelIndex);
  585. setScopedVars(rowCopy, option);
  586. const rowHeight = this.getRowHeight(rowCopy);
  587. const rowPanels = rowCopy.panels || [];
  588. let panelBelowIndex;
  589. if (panel.collapsed) {
  590. // For collapsed row just copy its panels and set scoped vars and proper IDs
  591. for (const rowPanel of rowPanels) {
  592. setScopedVars(rowPanel, option);
  593. if (optionIndex > 0) {
  594. this.updateRepeatedPanelIds(rowPanel, true);
  595. }
  596. }
  597. rowCopy.gridPos.y += optionIndex;
  598. yPos += optionIndex;
  599. panelBelowIndex = panelIndex + optionIndex + 1;
  600. } else {
  601. // insert after 'row' panel
  602. const insertPos = panelIndex + (rowPanels.length + 1) * optionIndex + 1;
  603. rowPanels.forEach((rowPanel: PanelModel, i: number) => {
  604. setScopedVars(rowPanel, option);
  605. if (optionIndex > 0) {
  606. const cloneRowPanel = new PanelModel(rowPanel);
  607. this.updateRepeatedPanelIds(cloneRowPanel, true);
  608. // For exposed row additionally set proper Y grid position and add it to dashboard panels
  609. cloneRowPanel.gridPos.y += rowHeight * optionIndex;
  610. this.panels.splice(insertPos + i, 0, cloneRowPanel);
  611. }
  612. });
  613. rowCopy.panels = [];
  614. rowCopy.gridPos.y += rowHeight * optionIndex;
  615. yPos += rowHeight;
  616. panelBelowIndex = insertPos + rowPanels.length;
  617. }
  618. // Update gridPos for panels below if we inserted more than 1 repeated row panel
  619. if (selectedOptions.length > 1) {
  620. for (const panel of this.panels.slice(panelBelowIndex)) {
  621. panel.gridPos.y += yPos;
  622. }
  623. }
  624. }
  625. }
  626. updateRepeatedPanelIds(panel: PanelModel, repeatedByRow?: boolean) {
  627. panel.repeatPanelId = panel.id;
  628. panel.id = this.getNextPanelId();
  629. if (repeatedByRow) {
  630. panel.repeatedByRow = true;
  631. } else {
  632. panel.repeat = undefined;
  633. }
  634. return panel;
  635. }
  636. getSelectedVariableOptions(variable: any) {
  637. let selectedOptions: any[];
  638. if (isAllVariable(variable)) {
  639. selectedOptions = variable.options.slice(1, variable.options.length);
  640. } else {
  641. selectedOptions = filter(variable.options, { selected: true });
  642. }
  643. return selectedOptions;
  644. }
  645. getRowHeight(rowPanel: PanelModel): number {
  646. if (!rowPanel.panels || rowPanel.panels.length === 0) {
  647. return 0;
  648. }
  649. const rowYPos = rowPanel.gridPos.y;
  650. const positions = map(rowPanel.panels, 'gridPos');
  651. const maxPos = maxBy(positions, (pos: GridPos) => pos.y + pos.h);
  652. return maxPos!.y + maxPos!.h - rowYPos;
  653. }
  654. removePanel(panel: PanelModel) {
  655. this.panels = this.panels.filter((item) => item !== panel);
  656. this.events.publish(new DashboardPanelsChangedEvent());
  657. }
  658. removeRow(row: PanelModel, removePanels: boolean) {
  659. const needToggle = (!removePanels && row.collapsed) || (removePanels && !row.collapsed);
  660. if (needToggle) {
  661. this.toggleRow(row);
  662. }
  663. this.removePanel(row);
  664. }
  665. expandRows() {
  666. const collapsedRows = this.panels.filter((p) => p.type === 'row' && p.collapsed);
  667. for (const row of collapsedRows) {
  668. this.toggleRow(row);
  669. }
  670. }
  671. collapseRows() {
  672. const collapsedRows = this.panels.filter((p) => p.type === 'row' && !p.collapsed);
  673. for (const row of collapsedRows) {
  674. this.toggleRow(row);
  675. }
  676. }
  677. isSubMenuVisible() {
  678. return (
  679. this.links.length > 0 ||
  680. this.getVariables().some((variable) => variable.hide !== 2) ||
  681. this.annotations.list.some((annotation) => !annotation.hide)
  682. );
  683. }
  684. getPanelInfoById(panelId: number) {
  685. const panelIndex = this.panels.findIndex((p) => p.id === panelId);
  686. return panelIndex >= 0 ? { panel: this.panels[panelIndex], index: panelIndex } : null;
  687. }
  688. duplicatePanel(panel: PanelModel) {
  689. const newPanel = panel.getSaveModel();
  690. newPanel.id = this.getNextPanelId();
  691. delete newPanel.repeat;
  692. delete newPanel.repeatIteration;
  693. delete newPanel.repeatPanelId;
  694. delete newPanel.scopedVars;
  695. if (newPanel.alert) {
  696. delete newPanel.thresholds;
  697. }
  698. delete newPanel.alert;
  699. // does it fit to the right?
  700. if (panel.gridPos.x + panel.gridPos.w * 2 <= GRID_COLUMN_COUNT) {
  701. newPanel.gridPos.x += panel.gridPos.w;
  702. } else {
  703. // add below
  704. newPanel.gridPos.y += panel.gridPos.h;
  705. }
  706. this.addPanel(newPanel);
  707. return newPanel;
  708. }
  709. formatDate(date: DateTimeInput, format?: string) {
  710. return dateTimeFormat(date, {
  711. format,
  712. timeZone: this.getTimezone(),
  713. });
  714. }
  715. destroy() {
  716. this.appEventsSubscription.unsubscribe();
  717. this.events.removeAllListeners();
  718. for (const panel of this.panels) {
  719. panel.destroy();
  720. }
  721. }
  722. toggleRow(row: PanelModel) {
  723. const rowIndex = indexOf(this.panels, row);
  724. if (!row.collapsed) {
  725. const rowPanels = this.getRowPanels(rowIndex);
  726. // remove panels
  727. pull(this.panels, ...rowPanels);
  728. // save panel models inside row panel
  729. row.panels = rowPanels.map((panel: PanelModel) => panel.getSaveModel());
  730. row.collapsed = true;
  731. if (rowPanels.some((panel) => panel.hasChanged)) {
  732. row.configRev++;
  733. }
  734. // emit change event
  735. this.events.publish(new DashboardPanelsChangedEvent());
  736. return;
  737. }
  738. row.collapsed = false;
  739. const rowPanels = row.panels ?? [];
  740. const hasRepeat = rowPanels.some((p: PanelModel) => p.repeat);
  741. if (rowPanels.length > 0) {
  742. // Use first panel to figure out if it was moved or pushed
  743. // If the panel doesn't have gridPos.y, use the row gridPos.y instead.
  744. // This can happen for some generated dashboards.
  745. const firstPanelYPos = rowPanels[0].gridPos.y ?? row.gridPos.y;
  746. const yDiff = firstPanelYPos - (row.gridPos.y + row.gridPos.h);
  747. // start inserting after row
  748. let insertPos = rowIndex + 1;
  749. // y max will represent the bottom y pos after all panels have been added
  750. // needed to know home much panels below should be pushed down
  751. let yMax = row.gridPos.y;
  752. for (const panel of rowPanels) {
  753. // set the y gridPos if it wasn't already set
  754. panel.gridPos.y ?? (panel.gridPos.y = row.gridPos.y); // (Safari 13.1 lacks ??= support)
  755. // make sure y is adjusted (in case row moved while collapsed)
  756. panel.gridPos.y -= yDiff;
  757. // insert after row
  758. this.panels.splice(insertPos, 0, new PanelModel(panel));
  759. // update insert post and y max
  760. insertPos += 1;
  761. yMax = Math.max(yMax, panel.gridPos.y + panel.gridPos.h);
  762. }
  763. const pushDownAmount = yMax - row.gridPos.y - 1;
  764. // push panels below down
  765. for (const panel of this.panels.slice(insertPos)) {
  766. panel.gridPos.y += pushDownAmount;
  767. }
  768. row.panels = [];
  769. if (hasRepeat) {
  770. this.processRowRepeats(row);
  771. }
  772. }
  773. // sort panels
  774. this.sortPanelsByGridPos();
  775. // emit change event
  776. this.events.publish(new DashboardPanelsChangedEvent());
  777. }
  778. /**
  779. * Will return all panels after rowIndex until it encounters another row
  780. */
  781. getRowPanels(rowIndex: number): PanelModel[] {
  782. const panelsBelowRow = this.panels.slice(rowIndex + 1);
  783. const nextRowIndex = panelsBelowRow.findIndex((p) => p.type === 'row');
  784. // Take all panels up to next row, or all panels if there are no other rows
  785. const rowPanels = panelsBelowRow.slice(0, nextRowIndex >= 0 ? nextRowIndex : this.panels.length);
  786. return rowPanels;
  787. }
  788. /** @deprecated */
  789. on<T>(event: AppEvent<T>, callback: (payload?: T) => void) {
  790. console.log('DashboardModel.on is deprecated use events.subscribe');
  791. this.events.on(event, callback);
  792. }
  793. /** @deprecated */
  794. off<T>(event: AppEvent<T>, callback: (payload?: T) => void) {
  795. console.log('DashboardModel.off is deprecated');
  796. this.events.off(event, callback);
  797. }
  798. cycleGraphTooltip() {
  799. this.graphTooltip = (this.graphTooltip + 1) % 3;
  800. }
  801. sharedTooltipModeEnabled() {
  802. return this.graphTooltip > 0;
  803. }
  804. sharedCrosshairModeOnly() {
  805. return this.graphTooltip === 1;
  806. }
  807. getRelativeTime(date: DateTimeInput) {
  808. return dateTimeFormatTimeAgo(date, {
  809. timeZone: this.getTimezone(),
  810. });
  811. }
  812. isSnapshot() {
  813. return this.snapshot !== undefined;
  814. }
  815. getTimezone(): TimeZone {
  816. return (this.timezone ? this.timezone : contextSrv?.user?.timezone) as TimeZone;
  817. }
  818. private updateSchema(old: any) {
  819. const migrator = new DashboardMigrator(this);
  820. migrator.updateSchema(old);
  821. }
  822. resetOriginalTime() {
  823. this.originalTime = cloneDeep(this.time);
  824. }
  825. hasTimeChanged() {
  826. const { time, originalTime } = this;
  827. // Compare moment values vs strings values
  828. return !(
  829. isEqual(time, originalTime) ||
  830. (isEqual(dateTime(time?.from), dateTime(originalTime?.from)) &&
  831. isEqual(dateTime(time?.to), dateTime(originalTime?.to)))
  832. );
  833. }
  834. resetOriginalVariables(initial = false) {
  835. if (initial) {
  836. this.originalTemplating = this.cloneVariablesFrom(this.templating.list);
  837. return;
  838. }
  839. this.originalTemplating = this.cloneVariablesFrom(this.getVariablesFromState(this.uid));
  840. }
  841. hasVariableValuesChanged() {
  842. return this.hasVariablesChanged(this.originalTemplating, this.getVariablesFromState(this.uid));
  843. }
  844. autoFitPanels(viewHeight: number, kioskMode?: UrlQueryValue) {
  845. const currentGridHeight = Math.max(...this.panels.map((panel) => panel.gridPos.h + panel.gridPos.y));
  846. const navbarHeight = 55;
  847. const margin = 20;
  848. const submenuHeight = 50;
  849. let visibleHeight = viewHeight - navbarHeight - margin;
  850. // Remove submenu height if visible
  851. if (this.meta.submenuEnabled && !kioskMode) {
  852. visibleHeight -= submenuHeight;
  853. }
  854. // add back navbar height
  855. if (kioskMode && kioskMode !== KioskMode.TV) {
  856. visibleHeight += navbarHeight;
  857. }
  858. const visibleGridHeight = Math.floor(visibleHeight / (GRID_CELL_HEIGHT + GRID_CELL_VMARGIN));
  859. const scaleFactor = currentGridHeight / visibleGridHeight;
  860. for (const panel of this.panels) {
  861. panel.gridPos.y = Math.round(panel.gridPos.y / scaleFactor) || 1;
  862. panel.gridPos.h = Math.round(panel.gridPos.h / scaleFactor) || 1;
  863. }
  864. }
  865. templateVariableValueUpdated() {
  866. this.processRepeats();
  867. this.events.emit(CoreEvents.templateVariableValueUpdated);
  868. }
  869. getPanelByUrlId(panelUrlId: string) {
  870. const panelId = parseInt(panelUrlId ?? '0', 10);
  871. // First try to find it in a collapsed row and exand it
  872. const collapsedPanels = this.panels.filter((p) => p.collapsed);
  873. for (const panel of collapsedPanels) {
  874. const hasPanel = panel.panels?.some((rp: any) => rp.id === panelId);
  875. hasPanel && this.toggleRow(panel);
  876. }
  877. return this.getPanelById(panelId);
  878. }
  879. toggleLegendsForAll() {
  880. const panelsWithLegends = this.panels.filter(isPanelWithLegend);
  881. // determine if more panels are displaying legends or not
  882. const onCount = panelsWithLegends.filter((panel) => panel.legend.show).length;
  883. const offCount = panelsWithLegends.length - onCount;
  884. const panelLegendsOn = onCount >= offCount;
  885. for (const panel of panelsWithLegends) {
  886. panel.legend.show = !panelLegendsOn;
  887. panel.render();
  888. }
  889. }
  890. getVariables() {
  891. return this.getVariablesFromState(this.uid);
  892. }
  893. canEditAnnotations(dashboardId: number) {
  894. let canEdit = true;
  895. // if RBAC is enabled there are additional conditions to check
  896. if (contextSrv.accessControlEnabled()) {
  897. if (dashboardId === 0) {
  898. canEdit = !!this.meta.annotationsPermissions?.organization.canEdit;
  899. } else {
  900. canEdit = !!this.meta.annotationsPermissions?.dashboard.canEdit;
  901. }
  902. }
  903. return this.canEditDashboard() && canEdit;
  904. }
  905. canAddAnnotations() {
  906. // If RBAC is enabled there are additional conditions to check
  907. const canAdd = !contextSrv.accessControlEnabled() || this.meta.annotationsPermissions?.dashboard.canAdd;
  908. return this.canEditDashboard() && canAdd;
  909. }
  910. canEditDashboard() {
  911. return this.meta.canEdit || this.meta.canMakeEditable;
  912. }
  913. shouldUpdateDashboardPanelFromJSON(updatedPanel: PanelModel, panel: PanelModel) {
  914. const shouldUpdateGridPositionLayout = !isEqual(updatedPanel?.gridPos, panel?.gridPos);
  915. if (shouldUpdateGridPositionLayout) {
  916. this.events.publish(new DashboardPanelsChangedEvent());
  917. }
  918. }
  919. private getPanelRepeatVariable(panel: PanelModel) {
  920. return this.getVariablesFromState(this.uid).find((variable) => variable.name === panel.repeat);
  921. }
  922. private isSnapshotTruthy() {
  923. return this.snapshot;
  924. }
  925. private hasVariables() {
  926. return this.getVariablesFromState(this.uid).length > 0;
  927. }
  928. private hasVariablesChanged(originalVariables: any[], currentVariables: any[]): boolean {
  929. if (originalVariables.length !== currentVariables.length) {
  930. return false;
  931. }
  932. const updated = currentVariables.map((variable: any) => ({
  933. name: variable.name,
  934. type: variable.type,
  935. current: cloneDeep(variable.current),
  936. filters: cloneDeep(variable.filters),
  937. }));
  938. return !isEqual(updated, originalVariables);
  939. }
  940. private cloneVariablesFrom(variables: any[]): any[] {
  941. return variables.map((variable) => ({
  942. name: variable.name,
  943. type: variable.type,
  944. current: cloneDeep(variable.current),
  945. filters: cloneDeep(variable.filters),
  946. }));
  947. }
  948. private variablesTimeRangeProcessDoneHandler(event: VariablesTimeRangeProcessDone) {
  949. const processRepeats = event.payload.variableIds.length > 0;
  950. this.variablesChangedHandler(new VariablesChanged({ panelIds: [], refreshAll: true }), processRepeats);
  951. }
  952. private variablesChangedHandler(event: VariablesChanged, processRepeats = true) {
  953. if (processRepeats) {
  954. this.processRepeats();
  955. }
  956. if (event.payload.refreshAll || getTimeSrv().isRefreshOutsideThreshold(this.lastRefresh)) {
  957. this.startRefresh({ refreshAll: true, panelIds: [] });
  958. return;
  959. }
  960. if (this.panelInEdit || this.panelInView) {
  961. this.panelsAffectedByVariableChange = event.payload.panelIds.filter(
  962. (id) => id !== (this.panelInEdit?.id ?? this.panelInView?.id)
  963. );
  964. }
  965. this.startRefresh(event.payload);
  966. }
  967. private variablesChangedInUrlHandler(event: VariablesChangedInUrl) {
  968. this.templateVariableValueUpdated();
  969. this.startRefresh(event.payload);
  970. }
  971. }
  972. function isPanelWithLegend(panel: PanelModel): panel is PanelModel & Pick<Required<PanelModel>, 'legend'> {
  973. return Boolean(panel.legend);
  974. }