PanelModel.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612
  1. import { cloneDeep, defaultsDeep, isArray, isEqual, keys } from 'lodash';
  2. import { v4 as uuidv4 } from 'uuid';
  3. import {
  4. DataConfigSource,
  5. DataFrameDTO,
  6. DataLink,
  7. DataLinkBuiltInVars,
  8. DataQuery,
  9. DataTransformerConfig,
  10. EventBusSrv,
  11. FieldConfigSource,
  12. PanelPlugin,
  13. PanelPluginDataSupport,
  14. ScopedVars,
  15. urlUtil,
  16. PanelModel as IPanelModel,
  17. DataSourceRef,
  18. } from '@grafana/data';
  19. import { getTemplateSrv, RefreshEvent } from '@grafana/runtime';
  20. import config from 'app/core/config';
  21. import { getNextRefIdChar } from 'app/core/utils/query';
  22. import { QueryGroupOptions } from 'app/types';
  23. import {
  24. PanelOptionsChangedEvent,
  25. PanelQueriesChangedEvent,
  26. PanelTransformationsChangedEvent,
  27. RenderEvent,
  28. } from 'app/types/events';
  29. import { PanelModelLibraryPanel } from '../../library-panels/types';
  30. import { PanelQueryRunner } from '../../query/state/PanelQueryRunner';
  31. import { getVariablesUrlParams } from '../../variables/getAllVariableValuesForUrl';
  32. import { getTimeSrv } from '../services/TimeSrv';
  33. import { TimeOverrideResult } from '../utils/panel';
  34. import {
  35. filterFieldConfigOverrides,
  36. getPanelOptionsWithDefaults,
  37. isStandardFieldProp,
  38. restoreCustomOverrideRules,
  39. } from './getPanelOptionsWithDefaults';
  40. export interface GridPos {
  41. x: number;
  42. y: number;
  43. w: number;
  44. h: number;
  45. static?: boolean;
  46. }
  47. const notPersistedProperties: { [str: string]: boolean } = {
  48. events: true,
  49. isViewing: true,
  50. isEditing: true,
  51. isInView: true,
  52. hasRefreshed: true,
  53. cachedPluginOptions: true,
  54. plugin: true,
  55. queryRunner: true,
  56. replaceVariables: true,
  57. configRev: true,
  58. getDisplayTitle: true,
  59. dataSupport: true,
  60. key: true,
  61. };
  62. // For angular panels we need to clean up properties when changing type
  63. // To make sure the change happens without strange bugs happening when panels use same
  64. // named property with different type / value expectations
  65. // This is not required for react panels
  66. const mustKeepProps: { [str: string]: boolean } = {
  67. id: true,
  68. gridPos: true,
  69. type: true,
  70. title: true,
  71. scopedVars: true,
  72. repeat: true,
  73. repeatPanelId: true,
  74. repeatDirection: true,
  75. repeatedByRow: true,
  76. minSpan: true,
  77. collapsed: true,
  78. panels: true,
  79. targets: true,
  80. datasource: true,
  81. timeFrom: true,
  82. timeShift: true,
  83. hideTimeOverride: true,
  84. description: true,
  85. links: true,
  86. fullscreen: true,
  87. isEditing: true,
  88. hasRefreshed: true,
  89. events: true,
  90. cacheTimeout: true,
  91. cachedPluginOptions: true,
  92. transparent: true,
  93. pluginVersion: true,
  94. queryRunner: true,
  95. transformations: true,
  96. fieldConfig: true,
  97. maxDataPoints: true,
  98. interval: true,
  99. replaceVariables: true,
  100. libraryPanel: true,
  101. getDisplayTitle: true,
  102. configRev: true,
  103. key: true,
  104. };
  105. const defaults: any = {
  106. gridPos: { x: 0, y: 0, h: 3, w: 6 },
  107. targets: [{ refId: 'A' }],
  108. cachedPluginOptions: {},
  109. transparent: false,
  110. options: {},
  111. fieldConfig: {
  112. defaults: {},
  113. overrides: [],
  114. },
  115. title: '',
  116. };
  117. export class PanelModel implements DataConfigSource, IPanelModel {
  118. /* persisted id, used in URL to identify a panel */
  119. id!: number;
  120. gridPos!: GridPos;
  121. type!: string;
  122. title!: string;
  123. alert?: any;
  124. scopedVars?: ScopedVars;
  125. repeat?: string;
  126. repeatIteration?: number;
  127. repeatPanelId?: number;
  128. repeatDirection?: string;
  129. repeatedByRow?: boolean;
  130. maxPerRow?: number;
  131. collapsed?: boolean;
  132. panels?: PanelModel[];
  133. declare targets: DataQuery[];
  134. transformations?: DataTransformerConfig[];
  135. datasource: DataSourceRef | null = null;
  136. thresholds?: any;
  137. pluginVersion?: string;
  138. snapshotData?: DataFrameDTO[];
  139. timeFrom?: any;
  140. timeShift?: any;
  141. hideTimeOverride?: any;
  142. declare options: {
  143. [key: string]: any;
  144. };
  145. declare fieldConfig: FieldConfigSource;
  146. maxDataPoints?: number | null;
  147. interval?: string | null;
  148. description?: string;
  149. links?: DataLink[];
  150. declare transparent: boolean;
  151. libraryPanel?: { uid: undefined; name: string } | PanelModelLibraryPanel;
  152. // non persisted
  153. isViewing = false;
  154. isEditing = false;
  155. isInView = false;
  156. configRev = 0; // increments when configs change
  157. hasRefreshed?: boolean;
  158. cacheTimeout?: string | null;
  159. cachedPluginOptions: Record<string, PanelOptionsCache> = {};
  160. legend?: { show: boolean; sort?: string; sortDesc?: boolean };
  161. plugin?: PanelPlugin;
  162. /**
  163. * Unique in application state, this is used as redux key for panel and for redux panel state
  164. * Change will cause unmount and re-init of panel
  165. */
  166. key: string;
  167. /**
  168. * The PanelModel event bus only used for internal and legacy angular support.
  169. * The EventBus passed to panels is based on the dashboard event model.
  170. */
  171. events: EventBusSrv;
  172. private queryRunner?: PanelQueryRunner;
  173. constructor(model: any) {
  174. this.events = new EventBusSrv();
  175. this.restoreModel(model);
  176. this.replaceVariables = this.replaceVariables.bind(this);
  177. this.key = uuidv4();
  178. }
  179. /** Given a persistened PanelModel restores property values */
  180. restoreModel(model: any) {
  181. // Start with clean-up
  182. for (const property in this) {
  183. if (notPersistedProperties[property] || !this.hasOwnProperty(property)) {
  184. continue;
  185. }
  186. if (model[property]) {
  187. continue;
  188. }
  189. if (typeof (this as any)[property] === 'function') {
  190. continue;
  191. }
  192. if (typeof (this as any)[property] === 'symbol') {
  193. continue;
  194. }
  195. delete (this as any)[property];
  196. }
  197. // copy properties from persisted model
  198. for (const property in model) {
  199. (this as any)[property] = model[property];
  200. }
  201. // defaults
  202. defaultsDeep(this, cloneDeep(defaults));
  203. // queries must have refId
  204. this.ensureQueryIds();
  205. }
  206. generateNewKey() {
  207. this.key = uuidv4();
  208. }
  209. ensureQueryIds() {
  210. if (this.targets && isArray(this.targets)) {
  211. for (const query of this.targets) {
  212. if (!query.refId) {
  213. query.refId = getNextRefIdChar(this.targets);
  214. }
  215. }
  216. }
  217. }
  218. getOptions() {
  219. return this.options;
  220. }
  221. get hasChanged(): boolean {
  222. return this.configRev > 0;
  223. }
  224. updateOptions(options: object) {
  225. this.options = options;
  226. this.configRev++;
  227. this.events.publish(new PanelOptionsChangedEvent());
  228. this.render();
  229. }
  230. updateFieldConfig(config: FieldConfigSource) {
  231. this.fieldConfig = config;
  232. this.configRev++;
  233. this.events.publish(new PanelOptionsChangedEvent());
  234. this.resendLastResult();
  235. this.render();
  236. }
  237. getSaveModel() {
  238. const model: any = {};
  239. for (const property in this) {
  240. if (notPersistedProperties[property] || !this.hasOwnProperty(property)) {
  241. continue;
  242. }
  243. if (isEqual(this[property], defaults[property])) {
  244. continue;
  245. }
  246. model[property] = cloneDeep(this[property]);
  247. }
  248. return model;
  249. }
  250. setIsViewing(isViewing: boolean) {
  251. this.isViewing = isViewing;
  252. }
  253. updateGridPos(newPos: GridPos, manuallyUpdated = true) {
  254. if (
  255. newPos.x === this.gridPos.x &&
  256. newPos.y === this.gridPos.y &&
  257. newPos.h === this.gridPos.h &&
  258. newPos.w === this.gridPos.w
  259. ) {
  260. return;
  261. }
  262. this.gridPos.x = newPos.x;
  263. this.gridPos.y = newPos.y;
  264. this.gridPos.w = newPos.w;
  265. this.gridPos.h = newPos.h;
  266. if (manuallyUpdated) {
  267. this.configRev++;
  268. }
  269. }
  270. runAllPanelQueries(dashboardId: number, dashboardTimezone: string, timeData: TimeOverrideResult, width: number) {
  271. this.getQueryRunner().run({
  272. datasource: this.datasource,
  273. queries: this.targets,
  274. panelId: this.id,
  275. dashboardId: dashboardId,
  276. timezone: dashboardTimezone,
  277. timeRange: timeData.timeRange,
  278. timeInfo: timeData.timeInfo,
  279. maxDataPoints: this.maxDataPoints || Math.floor(width),
  280. minInterval: this.interval,
  281. scopedVars: this.scopedVars,
  282. cacheTimeout: this.cacheTimeout,
  283. transformations: this.transformations,
  284. });
  285. }
  286. refresh() {
  287. this.hasRefreshed = true;
  288. this.events.publish(new RefreshEvent());
  289. }
  290. render() {
  291. if (!this.hasRefreshed) {
  292. this.refresh();
  293. } else {
  294. this.events.publish(new RenderEvent());
  295. }
  296. }
  297. private getOptionsToRemember() {
  298. return Object.keys(this).reduce((acc, property) => {
  299. if (notPersistedProperties[property] || mustKeepProps[property]) {
  300. return acc;
  301. }
  302. return {
  303. ...acc,
  304. [property]: (this as any)[property],
  305. };
  306. }, {});
  307. }
  308. private restorePanelOptions(pluginId: string) {
  309. const prevOptions = this.cachedPluginOptions[pluginId];
  310. if (!prevOptions) {
  311. return;
  312. }
  313. Object.keys(prevOptions.properties).map((property) => {
  314. (this as any)[property] = prevOptions.properties[property];
  315. });
  316. this.fieldConfig = restoreCustomOverrideRules(this.fieldConfig, prevOptions.fieldConfig);
  317. }
  318. applyPluginOptionDefaults(plugin: PanelPlugin, isAfterPluginChange: boolean) {
  319. const options = getPanelOptionsWithDefaults({
  320. plugin,
  321. currentOptions: this.options,
  322. currentFieldConfig: this.fieldConfig,
  323. isAfterPluginChange: isAfterPluginChange,
  324. });
  325. this.fieldConfig = options.fieldConfig;
  326. this.options = options.options;
  327. }
  328. pluginLoaded(plugin: PanelPlugin) {
  329. this.plugin = plugin;
  330. const version = getPluginVersion(plugin);
  331. if (plugin.onPanelMigration) {
  332. if (version !== this.pluginVersion) {
  333. this.options = plugin.onPanelMigration(this);
  334. this.pluginVersion = version;
  335. }
  336. }
  337. this.applyPluginOptionDefaults(plugin, false);
  338. this.resendLastResult();
  339. }
  340. clearPropertiesBeforePluginChange() {
  341. // remove panel type specific options
  342. for (const key of keys(this)) {
  343. if (mustKeepProps[key]) {
  344. continue;
  345. }
  346. delete (this as any)[key];
  347. }
  348. this.options = {};
  349. // clear custom options
  350. this.fieldConfig = {
  351. defaults: {
  352. ...this.fieldConfig.defaults,
  353. custom: {},
  354. },
  355. // filter out custom overrides
  356. overrides: filterFieldConfigOverrides(this.fieldConfig.overrides, isStandardFieldProp),
  357. };
  358. }
  359. changePlugin(newPlugin: PanelPlugin) {
  360. const pluginId = newPlugin.meta.id;
  361. const oldOptions: any = this.getOptionsToRemember();
  362. const prevFieldConfig = this.fieldConfig;
  363. const oldPluginId = this.type;
  364. const wasAngular = this.isAngularPlugin();
  365. this.cachedPluginOptions[oldPluginId] = {
  366. properties: oldOptions,
  367. fieldConfig: prevFieldConfig,
  368. };
  369. this.clearPropertiesBeforePluginChange();
  370. this.restorePanelOptions(pluginId);
  371. // Let panel plugins inspect options from previous panel and keep any that it can use
  372. if (newPlugin.onPanelTypeChanged) {
  373. const prevOptions = wasAngular ? { angular: oldOptions } : oldOptions.options;
  374. Object.assign(this.options, newPlugin.onPanelTypeChanged(this, oldPluginId, prevOptions, prevFieldConfig));
  375. }
  376. // switch
  377. this.type = pluginId;
  378. this.plugin = newPlugin;
  379. this.configRev++;
  380. this.applyPluginOptionDefaults(newPlugin, true);
  381. if (newPlugin.onPanelMigration) {
  382. this.pluginVersion = getPluginVersion(newPlugin);
  383. }
  384. }
  385. updateQueries(options: QueryGroupOptions) {
  386. const { dataSource } = options;
  387. this.datasource = {
  388. uid: dataSource.uid,
  389. type: dataSource.type,
  390. };
  391. this.cacheTimeout = options.cacheTimeout;
  392. this.timeFrom = options.timeRange?.from;
  393. this.timeShift = options.timeRange?.shift;
  394. this.hideTimeOverride = options.timeRange?.hide;
  395. this.interval = options.minInterval;
  396. this.maxDataPoints = options.maxDataPoints;
  397. this.targets = options.queries;
  398. this.configRev++;
  399. this.events.publish(new PanelQueriesChangedEvent());
  400. }
  401. addQuery(query?: Partial<DataQuery>) {
  402. query = query || { refId: 'A' };
  403. query.refId = getNextRefIdChar(this.targets);
  404. this.targets.push(query as DataQuery);
  405. this.configRev++;
  406. }
  407. changeQuery(query: DataQuery, index: number) {
  408. // ensure refId is maintained
  409. query.refId = this.targets[index].refId;
  410. this.configRev++;
  411. // update query in array
  412. this.targets = this.targets.map((item, itemIndex) => {
  413. if (itemIndex === index) {
  414. return query;
  415. }
  416. return item;
  417. });
  418. }
  419. getEditClone() {
  420. const sourceModel = this.getSaveModel();
  421. const clone = new PanelModel(sourceModel);
  422. clone.isEditing = true;
  423. const sourceQueryRunner = this.getQueryRunner();
  424. // Copy last query result
  425. clone.getQueryRunner().useLastResultFrom(sourceQueryRunner);
  426. return clone;
  427. }
  428. getTransformations() {
  429. return this.transformations;
  430. }
  431. getFieldOverrideOptions() {
  432. if (!this.plugin) {
  433. return undefined;
  434. }
  435. return {
  436. fieldConfig: this.fieldConfig,
  437. replaceVariables: this.replaceVariables,
  438. fieldConfigRegistry: this.plugin.fieldConfigRegistry,
  439. theme: config.theme2,
  440. };
  441. }
  442. getDataSupport(): PanelPluginDataSupport {
  443. return this.plugin?.dataSupport ?? { annotations: false, alertStates: false };
  444. }
  445. getQueryRunner(): PanelQueryRunner {
  446. if (!this.queryRunner) {
  447. this.queryRunner = new PanelQueryRunner(this);
  448. }
  449. return this.queryRunner;
  450. }
  451. hasTitle() {
  452. return this.title && this.title.length > 0;
  453. }
  454. isAngularPlugin(): boolean {
  455. return (this.plugin && this.plugin.angularPanelCtrl) !== undefined;
  456. }
  457. destroy() {
  458. this.events.removeAllListeners();
  459. if (this.queryRunner) {
  460. this.queryRunner.destroy();
  461. }
  462. }
  463. setTransformations(transformations: DataTransformerConfig[]) {
  464. this.transformations = transformations;
  465. this.resendLastResult();
  466. this.configRev++;
  467. this.events.publish(new PanelTransformationsChangedEvent());
  468. }
  469. setProperty(key: keyof this, value: any) {
  470. this[key] = value;
  471. this.configRev++;
  472. // Custom handling of repeat dependent options, handled here as PanelEditor can
  473. // update one key at a time right now
  474. if (key === 'repeat') {
  475. if (this.repeat && !this.repeatDirection) {
  476. this.repeatDirection = 'h';
  477. } else if (!this.repeat) {
  478. delete this.repeatDirection;
  479. delete this.maxPerRow;
  480. }
  481. }
  482. }
  483. replaceVariables(value: string, extraVars: ScopedVars | undefined, format?: string | Function) {
  484. const lastRequest = this.getQueryRunner().getLastRequest();
  485. const vars: ScopedVars = Object.assign({}, this.scopedVars, lastRequest?.scopedVars, extraVars);
  486. const allVariablesParams = getVariablesUrlParams(vars);
  487. const variablesQuery = urlUtil.toUrlParams(allVariablesParams);
  488. const timeRangeUrl = urlUtil.toUrlParams(getTimeSrv().timeRangeForUrl());
  489. vars[DataLinkBuiltInVars.keepTime] = {
  490. text: timeRangeUrl,
  491. value: timeRangeUrl,
  492. };
  493. vars[DataLinkBuiltInVars.includeVars] = {
  494. text: variablesQuery,
  495. value: variablesQuery,
  496. };
  497. return getTemplateSrv().replace(value, vars, format);
  498. }
  499. resendLastResult() {
  500. if (!this.plugin) {
  501. return;
  502. }
  503. this.getQueryRunner().resendLastResult();
  504. }
  505. /*
  506. * This is the title used when displaying the title in the UI so it will include any interpolated variables.
  507. * If you need the raw title without interpolation use title property instead.
  508. * */
  509. getDisplayTitle(): string {
  510. return this.replaceVariables(this.title, undefined, 'text');
  511. }
  512. }
  513. function getPluginVersion(plugin: PanelPlugin): string {
  514. return plugin && plugin.meta.info.version ? plugin.meta.info.version : config.buildInfo.version;
  515. }
  516. interface PanelOptionsCache {
  517. properties: any;
  518. fieldConfig: FieldConfigSource;
  519. }