PanelModel.test.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522
  1. import { ComponentClass } from 'react';
  2. import {
  3. DataLinkBuiltInVars,
  4. FieldConfigProperty,
  5. PanelData,
  6. PanelProps,
  7. standardEditorsRegistry,
  8. standardFieldConfigEditorRegistry,
  9. dateTime,
  10. TimeRange,
  11. } from '@grafana/data';
  12. import { setTemplateSrv } from '@grafana/runtime';
  13. import { queryBuilder } from 'app/features/variables/shared/testing/builders';
  14. import { mockStandardFieldConfigOptions } from '../../../../test/helpers/fieldConfig';
  15. import { getPanelPlugin } from '../../plugins/__mocks__/pluginMocks';
  16. import { PanelQueryRunner } from '../../query/state/PanelQueryRunner';
  17. import { TemplateSrv } from '../../templating/template_srv';
  18. import { variableAdapters } from '../../variables/adapters';
  19. import { createQueryVariableAdapter } from '../../variables/query/adapter';
  20. import { setTimeSrv } from '../services/TimeSrv';
  21. import { TimeOverrideResult } from '../utils/panel';
  22. import { PanelModel } from './PanelModel';
  23. standardFieldConfigEditorRegistry.setInit(() => mockStandardFieldConfigOptions());
  24. standardEditorsRegistry.setInit(() => mockStandardFieldConfigOptions());
  25. setTimeSrv({
  26. timeRangeForUrl: () => ({
  27. from: 1607687293000,
  28. to: 1607687293100,
  29. }),
  30. } as any);
  31. const getVariables = () => variablesMock;
  32. const getVariableWithName = (name: string) => variablesMock.filter((v) => v.name === name)[0];
  33. const getFilteredVariables = jest.fn();
  34. setTemplateSrv(
  35. new TemplateSrv({
  36. getVariables,
  37. getVariableWithName,
  38. getFilteredVariables,
  39. })
  40. );
  41. variableAdapters.setInit(() => [createQueryVariableAdapter()]);
  42. describe('PanelModel', () => {
  43. describe('when creating new panel model', () => {
  44. let model: any;
  45. let modelJson: any;
  46. let persistedOptionsMock;
  47. const tablePlugin = getPanelPlugin(
  48. {
  49. id: 'table',
  50. },
  51. null as unknown as ComponentClass<PanelProps>, // react
  52. {} // angular
  53. );
  54. tablePlugin.setPanelOptions((builder) => {
  55. builder.addBooleanSwitch({
  56. name: 'Show thresholds',
  57. path: 'showThresholds',
  58. defaultValue: true,
  59. description: '',
  60. });
  61. });
  62. tablePlugin.useFieldConfig({
  63. standardOptions: {
  64. [FieldConfigProperty.Unit]: {
  65. defaultValue: 'flop',
  66. },
  67. [FieldConfigProperty.Decimals]: {
  68. defaultValue: 2,
  69. },
  70. },
  71. useCustomConfig: (builder) => {
  72. builder.addBooleanSwitch({
  73. name: 'CustomProp',
  74. path: 'customProp',
  75. defaultValue: false,
  76. });
  77. },
  78. });
  79. beforeEach(() => {
  80. persistedOptionsMock = {
  81. fieldOptions: {
  82. thresholds: [
  83. {
  84. color: '#F2495C',
  85. index: 1,
  86. value: 50,
  87. },
  88. {
  89. color: '#73BF69',
  90. index: 0,
  91. value: null,
  92. },
  93. ],
  94. },
  95. arrayWith2Values: [{ name: 'changed to only one value' }],
  96. };
  97. modelJson = {
  98. type: 'table',
  99. maxDataPoints: 100,
  100. interval: '5m',
  101. showColumns: true,
  102. targets: [{ refId: 'A' }, { noRefId: true }],
  103. options: persistedOptionsMock,
  104. fieldConfig: {
  105. defaults: {
  106. unit: 'mpg',
  107. thresholds: {
  108. mode: 'absolute',
  109. steps: [
  110. { color: 'green', value: null },
  111. { color: 'red', value: 80 },
  112. ],
  113. },
  114. },
  115. overrides: [
  116. {
  117. matcher: {
  118. id: '1',
  119. options: {},
  120. },
  121. properties: [
  122. {
  123. id: 'thresholds',
  124. value: {
  125. mode: 'absolute',
  126. steps: [
  127. { color: 'green', value: null },
  128. { color: 'red', value: 80 },
  129. ],
  130. },
  131. },
  132. ],
  133. },
  134. ],
  135. },
  136. };
  137. model = new PanelModel(modelJson);
  138. model.pluginLoaded(tablePlugin);
  139. });
  140. it('should apply defaults', () => {
  141. expect(model.gridPos.h).toBe(3);
  142. });
  143. it('should apply option defaults', () => {
  144. expect(model.getOptions().showThresholds).toBeTruthy();
  145. });
  146. it('should change null thresholds to negative infinity', () => {
  147. expect(model.fieldConfig.defaults.thresholds.steps[0].value).toBe(-Infinity);
  148. expect(model.fieldConfig.overrides[0].properties[0].value.steps[0].value).toBe(-Infinity);
  149. });
  150. it('should apply option defaults but not override if array is changed', () => {
  151. expect(model.getOptions().arrayWith2Values.length).toBe(1);
  152. });
  153. it('should apply field config defaults', () => {
  154. // default unit is overriden by model
  155. expect(model.getFieldOverrideOptions().fieldConfig.defaults.unit).toBe('mpg');
  156. // default decimals are aplied
  157. expect(model.getFieldOverrideOptions().fieldConfig.defaults.decimals).toBe(2);
  158. });
  159. it('should set model props on instance', () => {
  160. expect(model.showColumns).toBe(true);
  161. });
  162. it('should add missing refIds', () => {
  163. expect(model.targets[1].refId).toBe('B');
  164. });
  165. it("shouldn't break panel with non-array targets", () => {
  166. modelJson.targets = {
  167. 0: { refId: 'A' },
  168. foo: { bar: 'baz' },
  169. };
  170. model = new PanelModel(modelJson);
  171. expect(model.targets[0].refId).toBe('A');
  172. });
  173. it('getSaveModel should remove defaults', () => {
  174. const saveModel = model.getSaveModel();
  175. expect(saveModel.gridPos).toBe(undefined);
  176. });
  177. it('getSaveModel should remove nonPersistedProperties', () => {
  178. const saveModel = model.getSaveModel();
  179. expect(saveModel.events).toBe(undefined);
  180. });
  181. describe('variables interpolation', () => {
  182. beforeEach(() => {
  183. model.scopedVars = {
  184. aaa: { value: 'AAA', text: 'upperA' },
  185. bbb: { value: 'BBB', text: 'upperB' },
  186. };
  187. });
  188. it('should interpolate variables', () => {
  189. const out = model.replaceVariables('hello $aaa');
  190. expect(out).toBe('hello AAA');
  191. });
  192. it('should interpolate $__url_time_range variable', () => {
  193. const out = model.replaceVariables(`/d/1?$${DataLinkBuiltInVars.keepTime}`);
  194. expect(out).toBe('/d/1?from=1607687293000&to=1607687293100');
  195. });
  196. it('should interpolate $__all_variables variable', () => {
  197. const out = model.replaceVariables(`/d/1?$${DataLinkBuiltInVars.includeVars}`);
  198. expect(out).toBe('/d/1?var-test1=val1&var-test2=val2&var-test3=Value%203&var-test4=A&var-test4=B');
  199. });
  200. it('should prefer the local variable value', () => {
  201. const extra = { aaa: { text: '???', value: 'XXX' } };
  202. const out = model.replaceVariables('hello $aaa and $bbb', extra);
  203. expect(out).toBe('hello XXX and BBB');
  204. });
  205. it('Can use request scoped vars', () => {
  206. model.getQueryRunner().getLastRequest = () => {
  207. return {
  208. scopedVars: {
  209. __interval: { text: '10m', value: '10m' },
  210. },
  211. };
  212. };
  213. const out = model.replaceVariables('hello $__interval');
  214. expect(out).toBe('hello 10m');
  215. });
  216. });
  217. describe('when changing panel type', () => {
  218. beforeEach(() => {
  219. const newPlugin = getPanelPlugin({ id: 'graph' });
  220. newPlugin.useFieldConfig({
  221. standardOptions: {
  222. [FieldConfigProperty.Color]: {
  223. settings: {
  224. byThresholdsSupport: true,
  225. },
  226. },
  227. },
  228. useCustomConfig: (builder) => {
  229. builder.addNumberInput({
  230. path: 'customProp',
  231. name: 'customProp',
  232. defaultValue: 100,
  233. });
  234. },
  235. });
  236. newPlugin.setPanelOptions((builder) => {
  237. builder.addBooleanSwitch({
  238. name: 'Show thresholds labels',
  239. path: 'showThresholdLabels',
  240. defaultValue: false,
  241. description: '',
  242. });
  243. });
  244. model.fieldConfig.defaults.decimals = 3;
  245. model.fieldConfig.defaults.custom = {
  246. customProp: true,
  247. };
  248. model.fieldConfig.overrides = [
  249. {
  250. matcher: { id: 'byName', options: 'D-series' },
  251. properties: [
  252. {
  253. id: 'custom.customProp',
  254. value: false,
  255. },
  256. {
  257. id: 'decimals',
  258. value: 0,
  259. },
  260. ],
  261. },
  262. ];
  263. model.changePlugin(newPlugin);
  264. model.alert = { id: 2 };
  265. });
  266. it('should keep maxDataPoints', () => {
  267. expect(model.maxDataPoints).toBe(100);
  268. });
  269. it('should keep interval', () => {
  270. expect(model.interval).toBe('5m');
  271. });
  272. it('should preseve standard field config', () => {
  273. expect(model.fieldConfig.defaults.decimals).toEqual(3);
  274. });
  275. it('should clear custom field config and apply new defaults', () => {
  276. expect(model.fieldConfig.defaults.custom).toEqual({
  277. customProp: 100,
  278. });
  279. });
  280. it('should remove overrides with custom props', () => {
  281. expect(model.fieldConfig.overrides.length).toEqual(1);
  282. expect(model.fieldConfig.overrides[0].properties[0].id).toEqual('decimals');
  283. });
  284. it('should apply next panel option defaults', () => {
  285. expect(model.getOptions().showThresholdLabels).toBeFalsy();
  286. expect(model.getOptions().showThresholds).toBeUndefined();
  287. });
  288. it('should remove table properties but keep core props', () => {
  289. expect(model.showColumns).toBe(undefined);
  290. });
  291. it('should restore table properties when changing back', () => {
  292. model.changePlugin(tablePlugin);
  293. expect(model.showColumns).toBe(true);
  294. });
  295. it('should restore custom field config to what it was and preserve standard options', () => {
  296. model.changePlugin(tablePlugin);
  297. expect(model.fieldConfig.defaults.custom.customProp).toBe(true);
  298. });
  299. it('should remove alert rule when changing type that does not support it', () => {
  300. model.changePlugin(getPanelPlugin({ id: 'table' }));
  301. expect(model.alert).toBe(undefined);
  302. });
  303. });
  304. describe('when changing to react panel from angular panel', () => {
  305. let panelQueryRunner: any;
  306. const onPanelTypeChanged = jest.fn();
  307. const reactPlugin = getPanelPlugin({ id: 'react' }).setPanelChangeHandler(onPanelTypeChanged as any);
  308. beforeEach(() => {
  309. model.changePlugin(reactPlugin);
  310. panelQueryRunner = model.getQueryRunner();
  311. });
  312. it('should call react onPanelTypeChanged', () => {
  313. expect(onPanelTypeChanged.mock.calls.length).toBe(1);
  314. expect(onPanelTypeChanged.mock.calls[0][1]).toBe('table');
  315. expect(onPanelTypeChanged.mock.calls[0][2].angular).toBeDefined();
  316. });
  317. it('getQueryRunner() should return same instance after changing to another react panel', () => {
  318. model.changePlugin(getPanelPlugin({ id: 'react2' }));
  319. const sameQueryRunner = model.getQueryRunner();
  320. expect(panelQueryRunner).toBe(sameQueryRunner);
  321. });
  322. });
  323. describe('variables interpolation', () => {
  324. let panelQueryRunner: any;
  325. const onPanelTypeChanged = jest.fn();
  326. const reactPlugin = getPanelPlugin({ id: 'react' }).setPanelChangeHandler(onPanelTypeChanged as any);
  327. beforeEach(() => {
  328. model.changePlugin(reactPlugin);
  329. panelQueryRunner = model.getQueryRunner();
  330. });
  331. it('should call react onPanelTypeChanged', () => {
  332. expect(onPanelTypeChanged.mock.calls.length).toBe(1);
  333. expect(onPanelTypeChanged.mock.calls[0][1]).toBe('table');
  334. expect(onPanelTypeChanged.mock.calls[0][2].angular).toBeDefined();
  335. });
  336. it('getQueryRunner() should return same instance after changing to another react panel', () => {
  337. model.changePlugin(getPanelPlugin({ id: 'react2' }));
  338. const sameQueryRunner = model.getQueryRunner();
  339. expect(panelQueryRunner).toBe(sameQueryRunner);
  340. });
  341. });
  342. describe('restoreModel', () => {
  343. it('Should clean state and set properties from model', () => {
  344. model.restoreModel({
  345. title: 'New title',
  346. options: { new: true },
  347. });
  348. expect(model.title).toBe('New title');
  349. expect(model.options.new).toBe(true);
  350. });
  351. it('Should delete properties that are now gone on new model', () => {
  352. model.someProperty = 'value';
  353. model.restoreModel({
  354. title: 'New title',
  355. options: {},
  356. });
  357. expect(model.someProperty).toBeUndefined();
  358. });
  359. it('Should remove old angular panel specific props', () => {
  360. model.axes = [{ prop: 1 }];
  361. model.thresholds = [];
  362. model.restoreModel({
  363. title: 'New title',
  364. options: {},
  365. });
  366. expect(model.axes).toBeUndefined();
  367. expect(model.thresholds).toBeUndefined();
  368. });
  369. it('Should be able to set defaults back to default', () => {
  370. model.transparent = true;
  371. model.restoreModel({});
  372. expect(model.transparent).toBe(false);
  373. });
  374. });
  375. describe('updateGridPos', () => {
  376. it('Should not have changes if no change', () => {
  377. model.gridPos = { w: 1, h: 1, x: 1, y: 2 };
  378. model.updateGridPos({ w: 1, h: 1, x: 1, y: 2 });
  379. expect(model.hasChanged).toBe(false);
  380. });
  381. it('Should have changes if gridPos is different', () => {
  382. model.gridPos = { w: 1, h: 1, x: 1, y: 2 };
  383. model.updateGridPos({ w: 10, h: 1, x: 1, y: 2 });
  384. expect(model.hasChanged).toBe(true);
  385. });
  386. it('Should not have changes if not manually updated', () => {
  387. model.gridPos = { w: 1, h: 1, x: 1, y: 2 };
  388. model.updateGridPos({ w: 10, h: 1, x: 1, y: 2 }, false);
  389. expect(model.hasChanged).toBe(false);
  390. });
  391. });
  392. describe('destroy', () => {
  393. it('Should still preserve last query result', () => {
  394. model.getQueryRunner().useLastResultFrom({
  395. getLastResult: () => ({} as PanelData),
  396. } as PanelQueryRunner);
  397. model.destroy();
  398. expect(model.getQueryRunner().getLastResult()).toBeDefined();
  399. });
  400. });
  401. describe('getDisplayTitle', () => {
  402. it('when called then it should interpolate singe value variables in title', () => {
  403. const model = new PanelModel({
  404. title: 'Single value variable [[test3]] ${test3} ${test3:percentencode}',
  405. });
  406. const title = model.getDisplayTitle();
  407. expect(title).toEqual('Single value variable Value 3 Value 3 Value%203');
  408. });
  409. it('when called then it should interpolate multi value variables in title', () => {
  410. const model = new PanelModel({
  411. title: 'Multi value variable [[test4]] ${test4} ${test4:percentencode}',
  412. });
  413. const title = model.getDisplayTitle();
  414. expect(title).toEqual('Multi value variable A + B A + B %7BA%2CB%7D');
  415. });
  416. });
  417. describe('runAllPanelQueries', () => {
  418. it('when called then it should call all pending queries', () => {
  419. model.getQueryRunner = jest.fn().mockReturnValue({
  420. run: jest.fn(),
  421. });
  422. const dashboardId = 123;
  423. const dashboardTimezone = 'browser';
  424. const width = 860;
  425. const timeData = {
  426. timeInfo: '',
  427. timeRange: {
  428. from: dateTime([2019, 1, 11, 12, 0]),
  429. to: dateTime([2019, 1, 11, 18, 0]),
  430. raw: {
  431. from: 'now-6h',
  432. to: 'now',
  433. },
  434. } as TimeRange,
  435. } as TimeOverrideResult;
  436. model.runAllPanelQueries(dashboardId, dashboardTimezone, timeData, width);
  437. expect(model.getQueryRunner).toBeCalled();
  438. });
  439. });
  440. });
  441. });
  442. const variablesMock = [
  443. queryBuilder().withId('test1').withName('test1').withCurrent('val1').build(),
  444. queryBuilder().withId('test2').withName('test2').withCurrent('val2').build(),
  445. queryBuilder().withId('test3').withName('test3').withCurrent('Value 3').build(),
  446. queryBuilder().withId('test4').withName('test4').withCurrent(['A', 'B']).build(),
  447. ];