DashboardMigrator.test.ts 62 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060
  1. import { each, map } from 'lodash';
  2. import { expect } from 'test/lib/common';
  3. import { DataLinkBuiltInVars, MappingType } from '@grafana/data';
  4. import { setDataSourceSrv } from '@grafana/runtime';
  5. import { config } from 'app/core/config';
  6. import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants';
  7. import { mockDataSource, MockDataSourceSrv } from 'app/features/alerting/unified/mocks';
  8. import { getPanelPlugin } from 'app/features/plugins/__mocks__/pluginMocks';
  9. import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
  10. import { VariableHide } from '../../variables/types';
  11. import { DashboardModel } from '../state/DashboardModel';
  12. import { PanelModel } from '../state/PanelModel';
  13. jest.mock('app/core/services/context_srv', () => ({}));
  14. const dataSources = {
  15. prom: mockDataSource({
  16. name: 'prom',
  17. uid: 'prom-uid',
  18. type: 'prometheus',
  19. }),
  20. prom2: mockDataSource({
  21. name: 'prom2',
  22. uid: 'prom2-uid',
  23. type: 'prometheus',
  24. isDefault: true,
  25. }),
  26. notDefault: mockDataSource({
  27. name: 'prom-not-default',
  28. uid: 'prom-not-default-uid',
  29. type: 'prometheus',
  30. isDefault: false,
  31. }),
  32. [MIXED_DATASOURCE_NAME]: mockDataSource({
  33. name: MIXED_DATASOURCE_NAME,
  34. type: 'mixed',
  35. uid: MIXED_DATASOURCE_NAME,
  36. }),
  37. };
  38. setDataSourceSrv(new MockDataSourceSrv(dataSources));
  39. describe('DashboardModel', () => {
  40. describe('when creating dashboard with old schema', () => {
  41. let model: any;
  42. let graph: any;
  43. let singlestat: any;
  44. let table: any;
  45. let singlestatGauge: any;
  46. config.panels = {
  47. stat: getPanelPlugin({ id: 'stat' }).meta,
  48. gauge: getPanelPlugin({ id: 'gauge' }).meta,
  49. };
  50. beforeEach(() => {
  51. model = new DashboardModel({
  52. services: {
  53. filter: { time: { from: 'now-1d', to: 'now' }, list: [{}] },
  54. },
  55. pulldowns: [
  56. { type: 'filtering', enable: true },
  57. { type: 'annotations', enable: true, annotations: [{ name: 'old' }] },
  58. ],
  59. panels: [
  60. {
  61. type: 'graph',
  62. legend: true,
  63. aliasYAxis: { test: 2 },
  64. y_formats: ['kbyte', 'ms'],
  65. grid: {
  66. min: 1,
  67. max: 10,
  68. rightMin: 5,
  69. rightMax: 15,
  70. leftLogBase: 1,
  71. rightLogBase: 2,
  72. threshold1: 200,
  73. threshold2: 400,
  74. threshold1Color: 'yellow',
  75. threshold2Color: 'red',
  76. },
  77. leftYAxisLabel: 'left label',
  78. targets: [{ refId: 'A' }, {}],
  79. },
  80. {
  81. type: 'singlestat',
  82. legend: true,
  83. thresholds: '10,20,30',
  84. colors: ['#FF0000', 'green', 'orange'],
  85. aliasYAxis: { test: 2 },
  86. grid: { min: 1, max: 10 },
  87. targets: [{ refId: 'A' }, {}],
  88. },
  89. {
  90. type: 'singlestat',
  91. thresholds: '10,20,30',
  92. colors: ['#FF0000', 'green', 'orange'],
  93. gauge: {
  94. show: true,
  95. thresholdMarkers: true,
  96. thresholdLabels: false,
  97. },
  98. grid: { min: 1, max: 10 },
  99. },
  100. {
  101. type: 'table',
  102. legend: true,
  103. styles: [{ thresholds: ['10', '20', '30'] }, { thresholds: ['100', '200', '300'] }],
  104. targets: [{ refId: 'A' }, {}],
  105. },
  106. ],
  107. });
  108. graph = model.panels[0];
  109. singlestat = model.panels[1];
  110. singlestatGauge = model.panels[2];
  111. table = model.panels[3];
  112. });
  113. it('should have title', () => {
  114. expect(model.title).toBe('No Title');
  115. });
  116. it('should have panel id', () => {
  117. expect(graph.id).toBe(1);
  118. });
  119. it('should move time and filtering list', () => {
  120. expect(model.time.from).toBe('now-1d');
  121. expect(model.templating.list[0].allFormat).toBe('glob');
  122. });
  123. it('graphite panel should change name too graph', () => {
  124. expect(graph.type).toBe('graph');
  125. });
  126. it('singlestat panel should be mapped to stat panel', () => {
  127. expect(singlestat.type).toBe('stat');
  128. expect(singlestat.fieldConfig.defaults.thresholds.steps[2].value).toBe(30);
  129. expect(singlestat.fieldConfig.defaults.thresholds.steps[0].color).toBe('#FF0000');
  130. });
  131. it('singlestat panel should be mapped to gauge panel', () => {
  132. expect(singlestatGauge.type).toBe('gauge');
  133. expect(singlestatGauge.options.showThresholdMarkers).toBe(true);
  134. expect(singlestatGauge.options.showThresholdLabels).toBe(false);
  135. });
  136. it('queries without refId should get it', () => {
  137. expect(graph.targets[1].refId).toBe('B');
  138. });
  139. it('update legend setting', () => {
  140. expect(graph.legend.show).toBe(true);
  141. });
  142. it('move aliasYAxis to series override', () => {
  143. expect(graph.seriesOverrides[0].alias).toBe('test');
  144. expect(graph.seriesOverrides[0].yaxis).toBe(2);
  145. });
  146. it('should move pulldowns to new schema', () => {
  147. expect(model.annotations.list[1].name).toBe('old');
  148. });
  149. it('table panel should only have two thresholds values', () => {
  150. expect(table.styles[0].thresholds[0]).toBe('20');
  151. expect(table.styles[0].thresholds[1]).toBe('30');
  152. expect(table.styles[1].thresholds[0]).toBe('200');
  153. expect(table.styles[1].thresholds[1]).toBe('300');
  154. });
  155. it('table type should be deprecated', () => {
  156. expect(table.type).toBe('table-old');
  157. });
  158. it('graph grid to yaxes options', () => {
  159. expect(graph.yaxes[0].min).toBe(1);
  160. expect(graph.yaxes[0].max).toBe(10);
  161. expect(graph.yaxes[0].format).toBe('kbyte');
  162. expect(graph.yaxes[0].label).toBe('left label');
  163. expect(graph.yaxes[0].logBase).toBe(1);
  164. expect(graph.yaxes[1].min).toBe(5);
  165. expect(graph.yaxes[1].max).toBe(15);
  166. expect(graph.yaxes[1].format).toBe('ms');
  167. expect(graph.yaxes[1].logBase).toBe(2);
  168. expect(graph.grid.rightMax).toBe(undefined);
  169. expect(graph.grid.rightLogBase).toBe(undefined);
  170. expect(graph.y_formats).toBe(undefined);
  171. });
  172. it('dashboard schema version should be set to latest', () => {
  173. expect(model.schemaVersion).toBe(36);
  174. });
  175. it('graph thresholds should be migrated', () => {
  176. expect(graph.thresholds.length).toBe(2);
  177. expect(graph.thresholds[0].op).toBe('gt');
  178. expect(graph.thresholds[0].value).toBe(200);
  179. expect(graph.thresholds[0].fillColor).toBe('yellow');
  180. expect(graph.thresholds[1].value).toBe(400);
  181. expect(graph.thresholds[1].fillColor).toBe('red');
  182. });
  183. it('graph thresholds should be migrated onto specified thresholds', () => {
  184. model = new DashboardModel({
  185. panels: [
  186. {
  187. type: 'graph',
  188. y_formats: ['kbyte', 'ms'],
  189. grid: {
  190. threshold1: 200,
  191. threshold2: 400,
  192. },
  193. thresholds: [{ value: 100 }],
  194. },
  195. ],
  196. });
  197. graph = model.panels[0];
  198. expect(graph.thresholds.length).toBe(3);
  199. expect(graph.thresholds[0].value).toBe(100);
  200. expect(graph.thresholds[1].value).toBe(200);
  201. expect(graph.thresholds[2].value).toBe(400);
  202. });
  203. });
  204. describe('when migrating to the grid layout', () => {
  205. let model: any;
  206. beforeEach(() => {
  207. model = {
  208. rows: [],
  209. };
  210. });
  211. it('should create proper grid', () => {
  212. model.rows = [createRow({ collapse: false, height: 8 }, [[6], [6]])];
  213. const dashboard = new DashboardModel(model);
  214. const panelGridPos = getGridPositions(dashboard);
  215. const expectedGrid = [
  216. { x: 0, y: 0, w: 12, h: 8 },
  217. { x: 12, y: 0, w: 12, h: 8 },
  218. ];
  219. expect(panelGridPos).toEqual(expectedGrid);
  220. });
  221. it('should add special "row" panel if row is collapsed', () => {
  222. model.rows = [createRow({ collapse: true, height: 8 }, [[6], [6]]), createRow({ height: 8 }, [[12]])];
  223. const dashboard = new DashboardModel(model);
  224. const panelGridPos = getGridPositions(dashboard);
  225. const expectedGrid = [
  226. { x: 0, y: 0, w: 24, h: 8 }, // row
  227. { x: 0, y: 1, w: 24, h: 8 }, // row
  228. { x: 0, y: 2, w: 24, h: 8 },
  229. ];
  230. expect(panelGridPos).toEqual(expectedGrid);
  231. });
  232. it('should add special "row" panel if row has visible title', () => {
  233. model.rows = [
  234. createRow({ showTitle: true, title: 'Row', height: 8 }, [[6], [6]]),
  235. createRow({ height: 8 }, [[12]]),
  236. ];
  237. const dashboard = new DashboardModel(model);
  238. const panelGridPos = getGridPositions(dashboard);
  239. const expectedGrid = [
  240. { x: 0, y: 0, w: 24, h: 8 }, // row
  241. { x: 0, y: 1, w: 12, h: 8 },
  242. { x: 12, y: 1, w: 12, h: 8 },
  243. { x: 0, y: 9, w: 24, h: 8 }, // row
  244. { x: 0, y: 10, w: 24, h: 8 },
  245. ];
  246. expect(panelGridPos).toEqual(expectedGrid);
  247. });
  248. it('should not add "row" panel if row has not visible title or not collapsed', () => {
  249. model.rows = [
  250. createRow({ collapse: true, height: 8 }, [[12]]),
  251. createRow({ height: 8 }, [[12]]),
  252. createRow({ height: 8 }, [[12], [6], [6]]),
  253. createRow({ collapse: true, height: 8 }, [[12]]),
  254. ];
  255. const dashboard = new DashboardModel(model);
  256. const panelGridPos = getGridPositions(dashboard);
  257. const expectedGrid = [
  258. { x: 0, y: 0, w: 24, h: 8 }, // row
  259. { x: 0, y: 1, w: 24, h: 8 }, // row
  260. { x: 0, y: 2, w: 24, h: 8 },
  261. { x: 0, y: 10, w: 24, h: 8 }, // row
  262. { x: 0, y: 11, w: 24, h: 8 },
  263. { x: 0, y: 19, w: 12, h: 8 },
  264. { x: 12, y: 19, w: 12, h: 8 },
  265. { x: 0, y: 27, w: 24, h: 8 }, // row
  266. ];
  267. expect(panelGridPos).toEqual(expectedGrid);
  268. });
  269. it('should add all rows if even one collapsed or titled row is present', () => {
  270. model.rows = [createRow({ collapse: true, height: 8 }, [[6], [6]]), createRow({ height: 8 }, [[12]])];
  271. const dashboard = new DashboardModel(model);
  272. const panelGridPos = getGridPositions(dashboard);
  273. const expectedGrid = [
  274. { x: 0, y: 0, w: 24, h: 8 }, // row
  275. { x: 0, y: 1, w: 24, h: 8 }, // row
  276. { x: 0, y: 2, w: 24, h: 8 },
  277. ];
  278. expect(panelGridPos).toEqual(expectedGrid);
  279. });
  280. it('should properly place panels with fixed height', () => {
  281. model.rows = [
  282. createRow({ height: 6 }, [[6], [6, 3], [6, 3]]),
  283. createRow({ height: 6 }, [[4], [4], [4, 3], [4, 3]]),
  284. ];
  285. const dashboard = new DashboardModel(model);
  286. const panelGridPos = getGridPositions(dashboard);
  287. const expectedGrid = [
  288. { x: 0, y: 0, w: 12, h: 6 },
  289. { x: 12, y: 0, w: 12, h: 3 },
  290. { x: 12, y: 3, w: 12, h: 3 },
  291. { x: 0, y: 6, w: 8, h: 6 },
  292. { x: 8, y: 6, w: 8, h: 6 },
  293. { x: 16, y: 6, w: 8, h: 3 },
  294. { x: 16, y: 9, w: 8, h: 3 },
  295. ];
  296. expect(panelGridPos).toEqual(expectedGrid);
  297. });
  298. it('should place panel to the right side of panel having bigger height', () => {
  299. model.rows = [createRow({ height: 6 }, [[4], [2, 3], [4, 6], [2, 3], [2, 3]])];
  300. const dashboard = new DashboardModel(model);
  301. const panelGridPos = getGridPositions(dashboard);
  302. const expectedGrid = [
  303. { x: 0, y: 0, w: 8, h: 6 },
  304. { x: 8, y: 0, w: 4, h: 3 },
  305. { x: 12, y: 0, w: 8, h: 6 },
  306. { x: 20, y: 0, w: 4, h: 3 },
  307. { x: 20, y: 3, w: 4, h: 3 },
  308. ];
  309. expect(panelGridPos).toEqual(expectedGrid);
  310. });
  311. it('should fill current row if it possible', () => {
  312. model.rows = [createRow({ height: 9 }, [[4], [2, 3], [4, 6], [2, 3], [2, 3], [8, 3]])];
  313. const dashboard = new DashboardModel(model);
  314. const panelGridPos = getGridPositions(dashboard);
  315. const expectedGrid = [
  316. { x: 0, y: 0, w: 8, h: 9 },
  317. { x: 8, y: 0, w: 4, h: 3 },
  318. { x: 12, y: 0, w: 8, h: 6 },
  319. { x: 20, y: 0, w: 4, h: 3 },
  320. { x: 20, y: 3, w: 4, h: 3 },
  321. { x: 8, y: 6, w: 16, h: 3 },
  322. ];
  323. expect(panelGridPos).toEqual(expectedGrid);
  324. });
  325. it('should fill current row if it possible (2)', () => {
  326. model.rows = [createRow({ height: 8 }, [[4], [2, 3], [4, 6], [2, 3], [2, 3], [8, 3]])];
  327. const dashboard = new DashboardModel(model);
  328. const panelGridPos = getGridPositions(dashboard);
  329. const expectedGrid = [
  330. { x: 0, y: 0, w: 8, h: 8 },
  331. { x: 8, y: 0, w: 4, h: 3 },
  332. { x: 12, y: 0, w: 8, h: 6 },
  333. { x: 20, y: 0, w: 4, h: 3 },
  334. { x: 20, y: 3, w: 4, h: 3 },
  335. { x: 8, y: 6, w: 16, h: 3 },
  336. ];
  337. expect(panelGridPos).toEqual(expectedGrid);
  338. });
  339. it('should fill current row if panel height more than row height', () => {
  340. model.rows = [createRow({ height: 6 }, [[4], [2, 3], [4, 8], [2, 3], [2, 3]])];
  341. const dashboard = new DashboardModel(model);
  342. const panelGridPos = getGridPositions(dashboard);
  343. const expectedGrid = [
  344. { x: 0, y: 0, w: 8, h: 6 },
  345. { x: 8, y: 0, w: 4, h: 3 },
  346. { x: 12, y: 0, w: 8, h: 8 },
  347. { x: 20, y: 0, w: 4, h: 3 },
  348. { x: 20, y: 3, w: 4, h: 3 },
  349. ];
  350. expect(panelGridPos).toEqual(expectedGrid);
  351. });
  352. it('should wrap panels to multiple rows', () => {
  353. model.rows = [createRow({ height: 6 }, [[6], [6], [12], [6], [3], [3]])];
  354. const dashboard = new DashboardModel(model);
  355. const panelGridPos = getGridPositions(dashboard);
  356. const expectedGrid = [
  357. { x: 0, y: 0, w: 12, h: 6 },
  358. { x: 12, y: 0, w: 12, h: 6 },
  359. { x: 0, y: 6, w: 24, h: 6 },
  360. { x: 0, y: 12, w: 12, h: 6 },
  361. { x: 12, y: 12, w: 6, h: 6 },
  362. { x: 18, y: 12, w: 6, h: 6 },
  363. ];
  364. expect(panelGridPos).toEqual(expectedGrid);
  365. });
  366. it('should add repeated row if repeat set', () => {
  367. model.rows = [
  368. createRow({ showTitle: true, title: 'Row', height: 8, repeat: 'server' }, [[6]]),
  369. createRow({ height: 8 }, [[12]]),
  370. ];
  371. const dashboard = new DashboardModel(model);
  372. const panelGridPos = getGridPositions(dashboard);
  373. const expectedGrid = [
  374. { x: 0, y: 0, w: 24, h: 8 },
  375. { x: 0, y: 1, w: 12, h: 8 },
  376. { x: 0, y: 9, w: 24, h: 8 },
  377. { x: 0, y: 10, w: 24, h: 8 },
  378. ];
  379. expect(panelGridPos).toEqual(expectedGrid);
  380. expect(dashboard.panels[0].repeat).toBe('server');
  381. expect(dashboard.panels[1].repeat).toBeUndefined();
  382. expect(dashboard.panels[2].repeat).toBeUndefined();
  383. expect(dashboard.panels[3].repeat).toBeUndefined();
  384. });
  385. it('should ignore repeated row', () => {
  386. model.rows = [
  387. createRow({ showTitle: true, title: 'Row1', height: 8, repeat: 'server' }, [[6]]),
  388. createRow(
  389. {
  390. showTitle: true,
  391. title: 'Row2',
  392. height: 8,
  393. repeatIteration: 12313,
  394. repeatRowId: 1,
  395. },
  396. [[6]]
  397. ),
  398. ];
  399. const dashboard = new DashboardModel(model);
  400. expect(dashboard.panels[0].repeat).toBe('server');
  401. expect(dashboard.panels.length).toBe(2);
  402. });
  403. it('should assign id', () => {
  404. model.rows = [createRow({ collapse: true, height: 8 }, [[6], [6]])];
  405. model.rows[0].panels[0] = {};
  406. const dashboard = new DashboardModel(model);
  407. expect(dashboard.panels[0].id).toBe(1);
  408. });
  409. });
  410. describe('when migrating from minSpan to maxPerRow', () => {
  411. it('maxPerRow should be correct', () => {
  412. const model = {
  413. panels: [{ minSpan: 8 }],
  414. };
  415. const dashboard = new DashboardModel(model);
  416. expect(dashboard.panels[0].maxPerRow).toBe(3);
  417. });
  418. });
  419. describe('when migrating panel links', () => {
  420. let model: any;
  421. beforeEach(() => {
  422. model = new DashboardModel({
  423. panels: [
  424. {
  425. links: [
  426. {
  427. url: 'http://mylink.com',
  428. keepTime: true,
  429. title: 'test',
  430. },
  431. {
  432. url: 'http://mylink.com?existingParam',
  433. params: 'customParam',
  434. title: 'test',
  435. },
  436. {
  437. url: 'http://mylink.com?existingParam',
  438. includeVars: true,
  439. title: 'test',
  440. },
  441. {
  442. dashboard: 'my other dashboard',
  443. title: 'test',
  444. },
  445. {
  446. dashUri: '',
  447. title: 'test',
  448. },
  449. {
  450. type: 'dashboard',
  451. keepTime: true,
  452. },
  453. ],
  454. },
  455. ],
  456. });
  457. });
  458. it('should add keepTime as variable', () => {
  459. expect(model.panels[0].links[0].url).toBe(`http://mylink.com?$${DataLinkBuiltInVars.keepTime}`);
  460. });
  461. it('should add params to url', () => {
  462. expect(model.panels[0].links[1].url).toBe('http://mylink.com?existingParam&customParam');
  463. });
  464. it('should add includeVars to url', () => {
  465. expect(model.panels[0].links[2].url).toBe(`http://mylink.com?existingParam&$${DataLinkBuiltInVars.includeVars}`);
  466. });
  467. it('should slugify dashboard name', () => {
  468. expect(model.panels[0].links[3].url).toBe(`dashboard/db/my-other-dashboard`);
  469. });
  470. });
  471. describe('when migrating variables', () => {
  472. let model: any;
  473. beforeEach(() => {
  474. model = new DashboardModel({
  475. panels: [
  476. {
  477. //graph panel
  478. options: {
  479. dataLinks: [
  480. {
  481. url: 'http://mylink.com?series=${__series_name}',
  482. },
  483. {
  484. url: 'http://mylink.com?series=${__value_time}',
  485. },
  486. ],
  487. },
  488. },
  489. {
  490. // panel with field options
  491. options: {
  492. fieldOptions: {
  493. defaults: {
  494. links: [
  495. {
  496. url: 'http://mylink.com?series=${__series_name}',
  497. },
  498. {
  499. url: 'http://mylink.com?series=${__value_time}',
  500. },
  501. ],
  502. title: '$__cell_0 * $__field_name * $__series_name',
  503. },
  504. },
  505. },
  506. },
  507. ],
  508. });
  509. });
  510. describe('data links', () => {
  511. it('should replace __series_name variable with __series.name', () => {
  512. expect(model.panels[0].options.dataLinks[0].url).toBe('http://mylink.com?series=${__series.name}');
  513. expect(model.panels[1].options.fieldOptions.defaults.links[0].url).toBe(
  514. 'http://mylink.com?series=${__series.name}'
  515. );
  516. });
  517. it('should replace __value_time variable with __value.time', () => {
  518. expect(model.panels[0].options.dataLinks[1].url).toBe('http://mylink.com?series=${__value.time}');
  519. expect(model.panels[1].options.fieldOptions.defaults.links[1].url).toBe(
  520. 'http://mylink.com?series=${__value.time}'
  521. );
  522. });
  523. });
  524. describe('field display', () => {
  525. it('should replace __series_name and __field_name variables with new syntax', () => {
  526. expect(model.panels[1].options.fieldOptions.defaults.title).toBe(
  527. '$__cell_0 * ${__field.name} * ${__series.name}'
  528. );
  529. });
  530. });
  531. });
  532. describe('when migrating labels from DataFrame to Field', () => {
  533. let model: any;
  534. beforeEach(() => {
  535. model = new DashboardModel({
  536. panels: [
  537. {
  538. //graph panel
  539. options: {
  540. dataLinks: [
  541. {
  542. url: 'http://mylink.com?series=${__series.labels}&${__series.labels.a}',
  543. },
  544. ],
  545. },
  546. },
  547. {
  548. // panel with field options
  549. options: {
  550. fieldOptions: {
  551. defaults: {
  552. links: [
  553. {
  554. url: 'http://mylink.com?series=${__series.labels}&${__series.labels.x}',
  555. },
  556. ],
  557. },
  558. },
  559. },
  560. },
  561. ],
  562. });
  563. });
  564. describe('data links', () => {
  565. it('should replace __series.label variable with __field.label', () => {
  566. expect(model.panels[0].options.dataLinks[0].url).toBe(
  567. 'http://mylink.com?series=${__field.labels}&${__field.labels.a}'
  568. );
  569. expect(model.panels[1].options.fieldOptions.defaults.links[0].url).toBe(
  570. 'http://mylink.com?series=${__field.labels}&${__field.labels.x}'
  571. );
  572. });
  573. });
  574. });
  575. describe('when migrating variables with multi support', () => {
  576. let model: DashboardModel;
  577. beforeEach(() => {
  578. model = new DashboardModel({
  579. templating: {
  580. list: [
  581. {
  582. multi: false,
  583. current: {
  584. value: ['value'],
  585. text: ['text'],
  586. },
  587. },
  588. {
  589. multi: true,
  590. current: {
  591. value: ['value'],
  592. text: ['text'],
  593. },
  594. },
  595. ],
  596. },
  597. });
  598. });
  599. it('should have two variables after migration', () => {
  600. expect(model.templating.list.length).toBe(2);
  601. });
  602. it('should be migrated if being out of sync', () => {
  603. expect(model.templating.list[0].multi).toBe(false);
  604. expect(model.templating.list[0].current).toEqual({
  605. text: 'text',
  606. value: 'value',
  607. });
  608. });
  609. it('should not be migrated if being in sync', () => {
  610. expect(model.templating.list[1].multi).toBe(true);
  611. expect(model.templating.list[1].current).toEqual({
  612. text: ['text'],
  613. value: ['value'],
  614. });
  615. });
  616. });
  617. describe('when migrating variables with tags', () => {
  618. let model: DashboardModel;
  619. beforeEach(() => {
  620. model = new DashboardModel({
  621. templating: {
  622. list: [
  623. {
  624. type: 'query',
  625. tags: ['Africa', 'America', 'Asia', 'Europe'],
  626. tagsQuery: 'select datacenter from x',
  627. tagValuesQuery: 'select value from x where datacenter = xyz',
  628. useTags: true,
  629. },
  630. {
  631. type: 'query',
  632. current: {
  633. tags: [
  634. {
  635. selected: true,
  636. text: 'America',
  637. values: ['server-us-east', 'server-us-central', 'server-us-west'],
  638. valuesText: 'server-us-east + server-us-central + server-us-west',
  639. },
  640. {
  641. selected: true,
  642. text: 'Europe',
  643. values: ['server-eu-east', 'server-eu-west'],
  644. valuesText: 'server-eu-east + server-eu-west',
  645. },
  646. ],
  647. text: 'server-us-east + server-us-central + server-us-west + server-eu-east + server-eu-west',
  648. value: ['server-us-east', 'server-us-central', 'server-us-west', 'server-eu-east', 'server-eu-west'],
  649. },
  650. tags: ['Africa', 'America', 'Asia', 'Europe'],
  651. tagsQuery: 'select datacenter from x',
  652. tagValuesQuery: 'select value from x where datacenter = xyz',
  653. useTags: true,
  654. },
  655. {
  656. type: 'query',
  657. tags: [
  658. { text: 'Africa', selected: false },
  659. { text: 'America', selected: true },
  660. { text: 'Asia', selected: false },
  661. { text: 'Europe', selected: false },
  662. ],
  663. tagsQuery: 'select datacenter from x',
  664. tagValuesQuery: 'select value from x where datacenter = xyz',
  665. useTags: true,
  666. },
  667. ],
  668. },
  669. });
  670. });
  671. it('should have three variables after migration', () => {
  672. expect(model.templating.list.length).toBe(3);
  673. });
  674. it('should have no tags', () => {
  675. expect(model.templating.list[0].tags).toBeUndefined();
  676. expect(model.templating.list[1].tags).toBeUndefined();
  677. expect(model.templating.list[2].tags).toBeUndefined();
  678. });
  679. it('should have no tagsQuery property', () => {
  680. expect(model.templating.list[0].tagsQuery).toBeUndefined();
  681. expect(model.templating.list[1].tagsQuery).toBeUndefined();
  682. expect(model.templating.list[2].tagsQuery).toBeUndefined();
  683. });
  684. it('should have no tagValuesQuery property', () => {
  685. expect(model.templating.list[0].tagValuesQuery).toBeUndefined();
  686. expect(model.templating.list[1].tagValuesQuery).toBeUndefined();
  687. expect(model.templating.list[2].tagValuesQuery).toBeUndefined();
  688. });
  689. it('should have no useTags property', () => {
  690. expect(model.templating.list[0].useTags).toBeUndefined();
  691. expect(model.templating.list[1].useTags).toBeUndefined();
  692. expect(model.templating.list[2].useTags).toBeUndefined();
  693. });
  694. });
  695. describe('when migrating to new Text Panel', () => {
  696. let model: DashboardModel;
  697. beforeEach(() => {
  698. model = new DashboardModel({
  699. panels: [
  700. {
  701. id: 2,
  702. type: 'text',
  703. title: 'Angular Text Panel',
  704. content:
  705. '# Angular Text Panel\n# $constant\n\nFor markdown syntax help: [commonmark.org/help](https://commonmark.org/help/)\n\n## $text\n\n',
  706. mode: 'markdown',
  707. },
  708. {
  709. id: 3,
  710. type: 'text2',
  711. title: 'React Text Panel from scratch',
  712. options: {
  713. mode: 'markdown',
  714. content:
  715. '# React Text Panel from scratch\n# $constant\n\nFor markdown syntax help: [commonmark.org/help](https://commonmark.org/help/)\n\n## $text',
  716. },
  717. },
  718. {
  719. id: 4,
  720. type: 'text2',
  721. title: 'React Text Panel from Angular Panel',
  722. options: {
  723. mode: 'markdown',
  724. content:
  725. '# React Text Panel from Angular Panel\n# $constant\n\nFor markdown syntax help: [commonmark.org/help](https://commonmark.org/help/)\n\n## $text',
  726. angular: {
  727. content:
  728. '# React Text Panel from Angular Panel\n# $constant\n\nFor markdown syntax help: [commonmark.org/help](https://commonmark.org/help/)\n\n## $text\n',
  729. mode: 'markdown',
  730. options: {},
  731. },
  732. },
  733. },
  734. ],
  735. });
  736. });
  737. it('should have 3 panels after migration', () => {
  738. expect(model.panels.length).toBe(3);
  739. });
  740. it('should not migrate panel with old Text Panel id', () => {
  741. const oldAngularPanel: any = model.panels[0];
  742. expect(oldAngularPanel.id).toEqual(2);
  743. expect(oldAngularPanel.type).toEqual('text');
  744. expect(oldAngularPanel.title).toEqual('Angular Text Panel');
  745. expect(oldAngularPanel.content).toEqual(
  746. '# Angular Text Panel\n# $constant\n\nFor markdown syntax help: [commonmark.org/help](https://commonmark.org/help/)\n\n## $text\n\n'
  747. );
  748. expect(oldAngularPanel.mode).toEqual('markdown');
  749. });
  750. it('should migrate panels with new Text Panel id', () => {
  751. const reactPanel: any = model.panels[1];
  752. expect(reactPanel.id).toEqual(3);
  753. expect(reactPanel.type).toEqual('text');
  754. expect(reactPanel.title).toEqual('React Text Panel from scratch');
  755. expect(reactPanel.options.content).toEqual(
  756. '# React Text Panel from scratch\n# $constant\n\nFor markdown syntax help: [commonmark.org/help](https://commonmark.org/help/)\n\n## $text'
  757. );
  758. expect(reactPanel.options.mode).toEqual('markdown');
  759. });
  760. it('should clean up old angular options for panels with new Text Panel id', () => {
  761. const reactPanel: any = model.panels[2];
  762. expect(reactPanel.id).toEqual(4);
  763. expect(reactPanel.type).toEqual('text');
  764. expect(reactPanel.title).toEqual('React Text Panel from Angular Panel');
  765. expect(reactPanel.options.content).toEqual(
  766. '# React Text Panel from Angular Panel\n# $constant\n\nFor markdown syntax help: [commonmark.org/help](https://commonmark.org/help/)\n\n## $text'
  767. );
  768. expect(reactPanel.options.mode).toEqual('markdown');
  769. expect(reactPanel.options.angular).toBeUndefined();
  770. });
  771. });
  772. describe('when migrating constant variables so they are always hidden', () => {
  773. let model: DashboardModel;
  774. beforeEach(() => {
  775. model = new DashboardModel({
  776. templating: {
  777. list: [
  778. {
  779. type: 'query',
  780. hide: VariableHide.dontHide,
  781. datasource: null,
  782. allFormat: '',
  783. },
  784. {
  785. type: 'query',
  786. hide: VariableHide.hideLabel,
  787. datasource: null,
  788. allFormat: '',
  789. },
  790. {
  791. type: 'query',
  792. hide: VariableHide.hideVariable,
  793. datasource: null,
  794. allFormat: '',
  795. },
  796. {
  797. type: 'constant',
  798. hide: VariableHide.dontHide,
  799. query: 'default value',
  800. current: { selected: true, text: 'A', value: 'B' },
  801. options: [{ selected: true, text: 'A', value: 'B' }],
  802. datasource: null,
  803. allFormat: '',
  804. },
  805. {
  806. type: 'constant',
  807. hide: VariableHide.hideLabel,
  808. query: 'default value',
  809. current: { selected: true, text: 'A', value: 'B' },
  810. options: [{ selected: true, text: 'A', value: 'B' }],
  811. datasource: null,
  812. allFormat: '',
  813. },
  814. {
  815. type: 'constant',
  816. hide: VariableHide.hideVariable,
  817. query: 'default value',
  818. current: { selected: true, text: 'A', value: 'B' },
  819. options: [{ selected: true, text: 'A', value: 'B' }],
  820. datasource: null,
  821. allFormat: '',
  822. },
  823. ],
  824. },
  825. });
  826. });
  827. it('should have six variables after migration', () => {
  828. expect(model.templating.list.length).toBe(6);
  829. });
  830. it('should not touch other variable types', () => {
  831. expect(model.templating.list[0].hide).toEqual(VariableHide.dontHide);
  832. expect(model.templating.list[1].hide).toEqual(VariableHide.hideLabel);
  833. expect(model.templating.list[2].hide).toEqual(VariableHide.hideVariable);
  834. });
  835. it('should migrate visible constant variables to textbox variables', () => {
  836. expect(model.templating.list[3]).toEqual({
  837. type: 'textbox',
  838. hide: VariableHide.dontHide,
  839. query: 'default value',
  840. current: { selected: true, text: 'default value', value: 'default value' },
  841. options: [{ selected: true, text: 'default value', value: 'default value' }],
  842. datasource: null,
  843. allFormat: '',
  844. });
  845. expect(model.templating.list[4]).toEqual({
  846. type: 'textbox',
  847. hide: VariableHide.hideLabel,
  848. query: 'default value',
  849. current: { selected: true, text: 'default value', value: 'default value' },
  850. options: [{ selected: true, text: 'default value', value: 'default value' }],
  851. datasource: null,
  852. allFormat: '',
  853. });
  854. });
  855. it('should change current and options for hidden constant variables', () => {
  856. expect(model.templating.list[5]).toEqual({
  857. type: 'constant',
  858. hide: VariableHide.hideVariable,
  859. query: 'default value',
  860. current: { selected: true, text: 'default value', value: 'default value' },
  861. options: [{ selected: true, text: 'default value', value: 'default value' }],
  862. datasource: null,
  863. allFormat: '',
  864. });
  865. });
  866. });
  867. describe('when migrating variable refresh to on dashboard load', () => {
  868. let model: DashboardModel;
  869. beforeEach(() => {
  870. model = new DashboardModel({
  871. templating: {
  872. list: [
  873. {
  874. type: 'query',
  875. name: 'variable_with_never_refresh_with_options',
  876. options: [{ text: 'A', value: 'A' }],
  877. refresh: 0,
  878. },
  879. {
  880. type: 'query',
  881. name: 'variable_with_never_refresh_without_options',
  882. options: [],
  883. refresh: 0,
  884. },
  885. {
  886. type: 'query',
  887. name: 'variable_with_dashboard_refresh_with_options',
  888. options: [{ text: 'A', value: 'A' }],
  889. refresh: 1,
  890. },
  891. {
  892. type: 'query',
  893. name: 'variable_with_dashboard_refresh_without_options',
  894. options: [],
  895. refresh: 1,
  896. },
  897. {
  898. type: 'query',
  899. name: 'variable_with_timerange_refresh_with_options',
  900. options: [{ text: 'A', value: 'A' }],
  901. refresh: 2,
  902. },
  903. {
  904. type: 'query',
  905. name: 'variable_with_timerange_refresh_without_options',
  906. options: [],
  907. refresh: 2,
  908. },
  909. {
  910. type: 'query',
  911. name: 'variable_with_no_refresh_with_options',
  912. options: [{ text: 'A', value: 'A' }],
  913. },
  914. {
  915. type: 'query',
  916. name: 'variable_with_no_refresh_without_options',
  917. options: [],
  918. },
  919. {
  920. type: 'query',
  921. name: 'variable_with_unknown_refresh_with_options',
  922. options: [{ text: 'A', value: 'A' }],
  923. refresh: 2001,
  924. },
  925. {
  926. type: 'query',
  927. name: 'variable_with_unknown_refresh_without_options',
  928. options: [],
  929. refresh: 2001,
  930. },
  931. {
  932. type: 'custom',
  933. name: 'custom',
  934. options: [{ text: 'custom', value: 'custom' }],
  935. },
  936. {
  937. type: 'textbox',
  938. name: 'textbox',
  939. options: [{ text: 'Hello', value: 'World' }],
  940. },
  941. {
  942. type: 'datasource',
  943. name: 'datasource',
  944. options: [{ text: 'ds', value: 'ds' }], // fake example doesn't exist
  945. },
  946. {
  947. type: 'interval',
  948. name: 'interval',
  949. options: [{ text: '1m', value: '1m' }],
  950. },
  951. ],
  952. },
  953. });
  954. });
  955. it('should have 11 variables after migration', () => {
  956. expect(model.templating.list.length).toBe(14);
  957. });
  958. it('should not affect custom variable types', () => {
  959. const custom = model.templating.list[10];
  960. expect(custom.type).toEqual('custom');
  961. expect(custom.options).toEqual([{ text: 'custom', value: 'custom' }]);
  962. });
  963. it('should not affect textbox variable types', () => {
  964. const textbox = model.templating.list[11];
  965. expect(textbox.type).toEqual('textbox');
  966. expect(textbox.options).toEqual([{ text: 'Hello', value: 'World' }]);
  967. });
  968. it('should not affect datasource variable types', () => {
  969. const datasource = model.templating.list[12];
  970. expect(datasource.type).toEqual('datasource');
  971. expect(datasource.options).toEqual([{ text: 'ds', value: 'ds' }]);
  972. });
  973. it('should not affect interval variable types', () => {
  974. const interval = model.templating.list[13];
  975. expect(interval.type).toEqual('interval');
  976. expect(interval.options).toEqual([{ text: '1m', value: '1m' }]);
  977. });
  978. it('should removed options from all query variables', () => {
  979. const queryVariables = model.templating.list.filter((v) => v.type === 'query');
  980. expect(queryVariables).toHaveLength(10);
  981. const noOfOptions = queryVariables.reduce((all, variable) => all + variable.options.length, 0);
  982. expect(noOfOptions).toBe(0);
  983. });
  984. it('should set the refresh prop to on dashboard load for all query variables that have never or unknown', () => {
  985. expect(model.templating.list[0].refresh).toBe(1);
  986. expect(model.templating.list[1].refresh).toBe(1);
  987. expect(model.templating.list[2].refresh).toBe(1);
  988. expect(model.templating.list[3].refresh).toBe(1);
  989. expect(model.templating.list[4].refresh).toBe(2);
  990. expect(model.templating.list[5].refresh).toBe(2);
  991. expect(model.templating.list[6].refresh).toBe(1);
  992. expect(model.templating.list[7].refresh).toBe(1);
  993. expect(model.templating.list[8].refresh).toBe(1);
  994. expect(model.templating.list[9].refresh).toBe(1);
  995. expect(model.templating.list[10].refresh).toBeUndefined();
  996. expect(model.templating.list[11].refresh).toBeUndefined();
  997. expect(model.templating.list[12].refresh).toBeUndefined();
  998. expect(model.templating.list[13].refresh).toBeUndefined();
  999. });
  1000. });
  1001. describe('when migrating old value mapping model', () => {
  1002. let model: DashboardModel;
  1003. beforeEach(() => {
  1004. model = new DashboardModel({
  1005. panels: [
  1006. {
  1007. id: 1,
  1008. type: 'timeseries',
  1009. fieldConfig: {
  1010. defaults: {
  1011. thresholds: {
  1012. mode: 'absolute',
  1013. steps: [
  1014. {
  1015. color: 'green',
  1016. value: null,
  1017. },
  1018. {
  1019. color: 'red',
  1020. value: 80,
  1021. },
  1022. ],
  1023. },
  1024. mappings: [
  1025. {
  1026. id: 0,
  1027. text: '1',
  1028. type: 1,
  1029. value: 'up',
  1030. },
  1031. {
  1032. id: 1,
  1033. text: 'BAD',
  1034. type: 1,
  1035. value: 'down',
  1036. },
  1037. {
  1038. from: '0',
  1039. id: 2,
  1040. text: 'below 30',
  1041. to: '30',
  1042. type: 2,
  1043. },
  1044. {
  1045. from: '30',
  1046. id: 3,
  1047. text: '100',
  1048. to: '100',
  1049. type: 2,
  1050. },
  1051. {
  1052. type: 1,
  1053. value: 'null',
  1054. text: 'it is null',
  1055. },
  1056. ],
  1057. },
  1058. overrides: [
  1059. {
  1060. matcher: { id: 'byName', options: 'D-series' },
  1061. properties: [
  1062. {
  1063. id: 'mappings',
  1064. value: [
  1065. {
  1066. id: 0,
  1067. text: 'OverrideText',
  1068. type: 1,
  1069. value: 'up',
  1070. },
  1071. ],
  1072. },
  1073. ],
  1074. },
  1075. ],
  1076. },
  1077. },
  1078. ],
  1079. });
  1080. });
  1081. it('should migrate value mapping model', () => {
  1082. expect(model.panels[0].fieldConfig.defaults.mappings).toEqual([
  1083. {
  1084. type: MappingType.ValueToText,
  1085. options: {
  1086. down: { text: 'BAD', color: undefined },
  1087. up: { text: '1', color: 'green' },
  1088. },
  1089. },
  1090. {
  1091. type: MappingType.RangeToText,
  1092. options: {
  1093. from: 0,
  1094. to: 30,
  1095. result: { text: 'below 30' },
  1096. },
  1097. },
  1098. {
  1099. type: MappingType.RangeToText,
  1100. options: {
  1101. from: 30,
  1102. to: 100,
  1103. result: { text: '100', color: 'red' },
  1104. },
  1105. },
  1106. {
  1107. type: MappingType.SpecialValue,
  1108. options: {
  1109. match: 'null',
  1110. result: { text: 'it is null', color: undefined },
  1111. },
  1112. },
  1113. ]);
  1114. expect(model.panels[0].fieldConfig.overrides).toEqual([
  1115. {
  1116. matcher: { id: 'byName', options: 'D-series' },
  1117. properties: [
  1118. {
  1119. id: 'mappings',
  1120. value: [
  1121. {
  1122. type: MappingType.ValueToText,
  1123. options: {
  1124. up: { text: 'OverrideText' },
  1125. },
  1126. },
  1127. ],
  1128. },
  1129. ],
  1130. },
  1131. ]);
  1132. });
  1133. });
  1134. describe('when migrating tooltipOptions to tooltip', () => {
  1135. it('should rename options.tooltipOptions to options.tooltip', () => {
  1136. const model = new DashboardModel({
  1137. panels: [
  1138. {
  1139. type: 'timeseries',
  1140. legend: true,
  1141. options: {
  1142. tooltipOptions: { mode: 'multi' },
  1143. },
  1144. },
  1145. {
  1146. type: 'xychart',
  1147. legend: true,
  1148. options: {
  1149. tooltipOptions: { mode: 'single' },
  1150. },
  1151. },
  1152. ],
  1153. });
  1154. expect(model.panels[0].options).toMatchInlineSnapshot(`
  1155. Object {
  1156. "tooltip": Object {
  1157. "mode": "multi",
  1158. },
  1159. }
  1160. `);
  1161. expect(model.panels[1].options).toMatchInlineSnapshot(`
  1162. Object {
  1163. "tooltip": Object {
  1164. "mode": "single",
  1165. },
  1166. }
  1167. `);
  1168. });
  1169. });
  1170. describe('when migrating singlestat value mappings', () => {
  1171. it('should migrate value mapping', () => {
  1172. const model = new DashboardModel({
  1173. panels: [
  1174. {
  1175. type: 'singlestat',
  1176. legend: true,
  1177. thresholds: '10,20,30',
  1178. colors: ['#FF0000', 'green', 'orange'],
  1179. aliasYAxis: { test: 2 },
  1180. grid: { min: 1, max: 10 },
  1181. targets: [{ refId: 'A' }, {}],
  1182. mappingType: 1,
  1183. mappingTypes: [
  1184. {
  1185. name: 'value to text',
  1186. value: 1,
  1187. },
  1188. ],
  1189. valueMaps: [
  1190. {
  1191. op: '=',
  1192. text: 'test',
  1193. value: '20',
  1194. },
  1195. {
  1196. op: '=',
  1197. text: 'test1',
  1198. value: '30',
  1199. },
  1200. {
  1201. op: '=',
  1202. text: '50',
  1203. value: '40',
  1204. },
  1205. ],
  1206. },
  1207. ],
  1208. });
  1209. expect(model.panels[0].fieldConfig.defaults.mappings).toMatchInlineSnapshot(`
  1210. Array [
  1211. Object {
  1212. "options": Object {
  1213. "20": Object {
  1214. "color": undefined,
  1215. "text": "test",
  1216. },
  1217. "30": Object {
  1218. "color": undefined,
  1219. "text": "test1",
  1220. },
  1221. "40": Object {
  1222. "color": "orange",
  1223. "text": "50",
  1224. },
  1225. },
  1226. "type": "value",
  1227. },
  1228. ]
  1229. `);
  1230. });
  1231. it('should migrate range mapping', () => {
  1232. const model = new DashboardModel({
  1233. panels: [
  1234. {
  1235. type: 'singlestat',
  1236. legend: true,
  1237. thresholds: '10,20,30',
  1238. colors: ['#FF0000', 'green', 'orange'],
  1239. aliasYAxis: { test: 2 },
  1240. grid: { min: 1, max: 10 },
  1241. targets: [{ refId: 'A' }, {}],
  1242. mappingType: 2,
  1243. mappingTypes: [
  1244. {
  1245. name: 'range to text',
  1246. value: 2,
  1247. },
  1248. ],
  1249. rangeMaps: [
  1250. {
  1251. from: '20',
  1252. to: '25',
  1253. text: 'text1',
  1254. },
  1255. {
  1256. from: '1',
  1257. to: '5',
  1258. text: 'text2',
  1259. },
  1260. {
  1261. from: '5',
  1262. to: '10',
  1263. text: '50',
  1264. },
  1265. ],
  1266. },
  1267. ],
  1268. });
  1269. expect(model.panels[0].fieldConfig.defaults.mappings).toMatchInlineSnapshot(`
  1270. Array [
  1271. Object {
  1272. "options": Object {
  1273. "from": 20,
  1274. "result": Object {
  1275. "color": undefined,
  1276. "text": "text1",
  1277. },
  1278. "to": 25,
  1279. },
  1280. "type": "range",
  1281. },
  1282. Object {
  1283. "options": Object {
  1284. "from": 1,
  1285. "result": Object {
  1286. "color": undefined,
  1287. "text": "text2",
  1288. },
  1289. "to": 5,
  1290. },
  1291. "type": "range",
  1292. },
  1293. Object {
  1294. "options": Object {
  1295. "from": 5,
  1296. "result": Object {
  1297. "color": "orange",
  1298. "text": "50",
  1299. },
  1300. "to": 10,
  1301. },
  1302. "type": "range",
  1303. },
  1304. ]
  1305. `);
  1306. });
  1307. });
  1308. describe('when migrating folded panel without fieldConfig.defaults', () => {
  1309. let model: DashboardModel;
  1310. beforeEach(() => {
  1311. model = new DashboardModel({
  1312. schemaVersion: 29,
  1313. panels: [
  1314. {
  1315. id: 1,
  1316. type: 'timeseries',
  1317. panels: [
  1318. {
  1319. id: 2,
  1320. fieldConfig: {
  1321. overrides: [
  1322. {
  1323. matcher: { id: 'byName', options: 'D-series' },
  1324. properties: [
  1325. {
  1326. id: 'displayName',
  1327. value: 'foobar',
  1328. },
  1329. ],
  1330. },
  1331. ],
  1332. },
  1333. },
  1334. ],
  1335. },
  1336. ],
  1337. });
  1338. });
  1339. it('should ignore fieldConfig.defaults', () => {
  1340. expect(model.panels[0].panels?.[0].fieldConfig.defaults).toEqual(undefined);
  1341. });
  1342. });
  1343. describe('labelsToFields should be split into two transformers', () => {
  1344. let model: DashboardModel;
  1345. beforeEach(() => {
  1346. model = new DashboardModel({
  1347. schemaVersion: 29,
  1348. panels: [
  1349. {
  1350. id: 1,
  1351. type: 'timeseries',
  1352. transformations: [{ id: 'labelsToFields' }],
  1353. },
  1354. ],
  1355. });
  1356. });
  1357. it('should create two transormatoins', () => {
  1358. const xforms = model.panels[0].transformations;
  1359. expect(xforms).toMatchInlineSnapshot(`
  1360. Array [
  1361. Object {
  1362. "id": "labelsToFields",
  1363. },
  1364. Object {
  1365. "id": "merge",
  1366. "options": Object {},
  1367. },
  1368. ]
  1369. `);
  1370. });
  1371. });
  1372. describe('migrating legacy CloudWatch queries', () => {
  1373. let model: any;
  1374. let panelTargets: any;
  1375. beforeEach(() => {
  1376. model = new DashboardModel({
  1377. annotations: {
  1378. list: [
  1379. {
  1380. actionPrefix: '',
  1381. alarmNamePrefix: '',
  1382. alias: '',
  1383. dimensions: {
  1384. InstanceId: 'i-123',
  1385. },
  1386. enable: true,
  1387. expression: '',
  1388. iconColor: 'red',
  1389. id: '',
  1390. matchExact: true,
  1391. metricName: 'CPUUtilization',
  1392. name: 'test',
  1393. namespace: 'AWS/EC2',
  1394. period: '',
  1395. prefixMatching: false,
  1396. region: 'us-east-2',
  1397. statistics: ['Minimum', 'Sum'],
  1398. },
  1399. ],
  1400. },
  1401. panels: [
  1402. {
  1403. gridPos: {
  1404. h: 8,
  1405. w: 12,
  1406. x: 0,
  1407. y: 0,
  1408. },
  1409. id: 4,
  1410. options: {
  1411. legend: {
  1412. calcs: [],
  1413. displayMode: 'list',
  1414. placement: 'bottom',
  1415. },
  1416. tooltipOptions: {
  1417. mode: 'single',
  1418. },
  1419. },
  1420. targets: [
  1421. {
  1422. alias: '',
  1423. dimensions: {
  1424. InstanceId: 'i-123',
  1425. },
  1426. expression: '',
  1427. id: '',
  1428. matchExact: true,
  1429. metricName: 'CPUUtilization',
  1430. namespace: 'AWS/EC2',
  1431. period: '',
  1432. refId: 'A',
  1433. region: 'default',
  1434. statistics: ['Average', 'Minimum', 'p12.21'],
  1435. },
  1436. {
  1437. alias: '',
  1438. dimensions: {
  1439. InstanceId: 'i-123',
  1440. },
  1441. expression: '',
  1442. hide: false,
  1443. id: '',
  1444. matchExact: true,
  1445. metricName: 'CPUUtilization',
  1446. namespace: 'AWS/EC2',
  1447. period: '',
  1448. refId: 'B',
  1449. region: 'us-east-2',
  1450. statistics: ['Sum'],
  1451. },
  1452. ],
  1453. title: 'Panel Title',
  1454. type: 'timeseries',
  1455. },
  1456. ],
  1457. });
  1458. panelTargets = model.panels[0].targets;
  1459. });
  1460. it('multiple stats query should have been split into three', () => {
  1461. expect(panelTargets.length).toBe(4);
  1462. });
  1463. it('new stats query should get the right statistic', () => {
  1464. expect(panelTargets[0].statistic).toBe('Average');
  1465. expect(panelTargets[1].statistic).toBe('Sum');
  1466. expect(panelTargets[2].statistic).toBe('Minimum');
  1467. expect(panelTargets[3].statistic).toBe('p12.21');
  1468. });
  1469. it('new stats queries should be put in the end of the array', () => {
  1470. expect(panelTargets[0].refId).toBe('A');
  1471. expect(panelTargets[1].refId).toBe('B');
  1472. expect(panelTargets[2].refId).toBe('C');
  1473. expect(panelTargets[3].refId).toBe('D');
  1474. });
  1475. describe('with nested panels', () => {
  1476. let panel1Targets: any;
  1477. let panel2Targets: any;
  1478. let nestedModel: DashboardModel;
  1479. beforeEach(() => {
  1480. nestedModel = new DashboardModel({
  1481. annotations: {
  1482. list: [
  1483. {
  1484. actionPrefix: '',
  1485. alarmNamePrefix: '',
  1486. alias: '',
  1487. dimensions: {
  1488. InstanceId: 'i-123',
  1489. },
  1490. enable: true,
  1491. expression: '',
  1492. iconColor: 'red',
  1493. id: '',
  1494. matchExact: true,
  1495. metricName: 'CPUUtilization',
  1496. name: 'test',
  1497. namespace: 'AWS/EC2',
  1498. period: '',
  1499. prefixMatching: false,
  1500. region: 'us-east-2',
  1501. statistics: ['Minimum', 'Sum'],
  1502. },
  1503. ],
  1504. },
  1505. panels: [
  1506. {
  1507. collapsed: false,
  1508. gridPos: {
  1509. h: 1,
  1510. w: 24,
  1511. x: 0,
  1512. y: 89,
  1513. },
  1514. id: 96,
  1515. title: 'DynamoDB',
  1516. type: 'row',
  1517. panels: [
  1518. {
  1519. gridPos: {
  1520. h: 8,
  1521. w: 12,
  1522. x: 0,
  1523. y: 0,
  1524. },
  1525. id: 4,
  1526. options: {
  1527. legend: {
  1528. calcs: [],
  1529. displayMode: 'list',
  1530. placement: 'bottom',
  1531. },
  1532. tooltipOptions: {
  1533. mode: 'single',
  1534. },
  1535. },
  1536. targets: [
  1537. {
  1538. alias: '',
  1539. dimensions: {
  1540. InstanceId: 'i-123',
  1541. },
  1542. expression: '',
  1543. id: '',
  1544. matchExact: true,
  1545. metricName: 'CPUUtilization',
  1546. namespace: 'AWS/EC2',
  1547. period: '',
  1548. refId: 'C',
  1549. region: 'default',
  1550. statistics: ['Average', 'Minimum', 'p12.21'],
  1551. },
  1552. {
  1553. alias: '',
  1554. dimensions: {
  1555. InstanceId: 'i-123',
  1556. },
  1557. expression: '',
  1558. hide: false,
  1559. id: '',
  1560. matchExact: true,
  1561. metricName: 'CPUUtilization',
  1562. namespace: 'AWS/EC2',
  1563. period: '',
  1564. refId: 'B',
  1565. region: 'us-east-2',
  1566. statistics: ['Sum'],
  1567. },
  1568. ],
  1569. title: 'Panel Title',
  1570. type: 'timeseries',
  1571. },
  1572. {
  1573. gridPos: {
  1574. h: 8,
  1575. w: 12,
  1576. x: 0,
  1577. y: 0,
  1578. },
  1579. id: 4,
  1580. options: {
  1581. legend: {
  1582. calcs: [],
  1583. displayMode: 'list',
  1584. placement: 'bottom',
  1585. },
  1586. tooltipOptions: {
  1587. mode: 'single',
  1588. },
  1589. },
  1590. targets: [
  1591. {
  1592. alias: '',
  1593. dimensions: {
  1594. InstanceId: 'i-123',
  1595. },
  1596. expression: '',
  1597. id: '',
  1598. matchExact: true,
  1599. metricName: 'CPUUtilization',
  1600. namespace: 'AWS/EC2',
  1601. period: '',
  1602. refId: 'A',
  1603. region: 'default',
  1604. statistics: ['Average'],
  1605. },
  1606. {
  1607. alias: '',
  1608. dimensions: {
  1609. InstanceId: 'i-123',
  1610. },
  1611. expression: '',
  1612. hide: false,
  1613. id: '',
  1614. matchExact: true,
  1615. metricName: 'CPUUtilization',
  1616. namespace: 'AWS/EC2',
  1617. period: '',
  1618. refId: 'B',
  1619. region: 'us-east-2',
  1620. statistics: ['Sum', 'Min'],
  1621. },
  1622. ],
  1623. title: 'Panel Title',
  1624. type: 'timeseries',
  1625. },
  1626. ],
  1627. },
  1628. ],
  1629. });
  1630. panel1Targets = nestedModel.panels[0].panels?.[0].targets;
  1631. panel2Targets = nestedModel.panels[0].panels?.[1].targets;
  1632. });
  1633. it('multiple stats query should have been split into one query per stat', () => {
  1634. expect(panel1Targets.length).toBe(4);
  1635. expect(panel2Targets.length).toBe(3);
  1636. });
  1637. it('new stats query should get the right statistic', () => {
  1638. expect(panel1Targets[0].statistic).toBe('Average');
  1639. expect(panel1Targets[1].statistic).toBe('Sum');
  1640. expect(panel1Targets[2].statistic).toBe('Minimum');
  1641. expect(panel1Targets[3].statistic).toBe('p12.21');
  1642. expect(panel2Targets[0].statistic).toBe('Average');
  1643. expect(panel2Targets[1].statistic).toBe('Sum');
  1644. expect(panel2Targets[2].statistic).toBe('Min');
  1645. });
  1646. it('new stats queries should be put in the end of the array', () => {
  1647. expect(panel1Targets[0].refId).toBe('C');
  1648. expect(panel1Targets[1].refId).toBe('B');
  1649. expect(panel1Targets[2].refId).toBe('A');
  1650. expect(panel1Targets[3].refId).toBe('D');
  1651. expect(panel2Targets[0].refId).toBe('A');
  1652. expect(panel2Targets[1].refId).toBe('B');
  1653. expect(panel2Targets[2].refId).toBe('C');
  1654. });
  1655. });
  1656. });
  1657. describe('when migrating datasource to refs', () => {
  1658. let model: DashboardModel;
  1659. beforeEach(() => {
  1660. model = new DashboardModel({
  1661. templating: {
  1662. list: [
  1663. {
  1664. type: 'query',
  1665. name: 'var',
  1666. options: [{ text: 'A', value: 'A' }],
  1667. refresh: 0,
  1668. datasource: 'prom',
  1669. },
  1670. ],
  1671. },
  1672. panels: [
  1673. {
  1674. id: 1,
  1675. datasource: 'prom',
  1676. },
  1677. {
  1678. id: 2,
  1679. datasource: null,
  1680. },
  1681. {
  1682. id: 3,
  1683. datasource: MIXED_DATASOURCE_NAME,
  1684. targets: [
  1685. {
  1686. datasource: 'prom',
  1687. },
  1688. ],
  1689. },
  1690. {
  1691. type: 'row',
  1692. id: 5,
  1693. panels: [
  1694. {
  1695. id: 6,
  1696. datasource: 'prom',
  1697. },
  1698. ],
  1699. },
  1700. ],
  1701. });
  1702. });
  1703. it('should not update variable datasource props to refs', () => {
  1704. expect(model.templating.list[0].datasource).toEqual('prom');
  1705. });
  1706. it('should update panel datasource props to refs for named data source', () => {
  1707. expect(model.panels[0].datasource).toEqual({ type: 'prometheus', uid: 'prom-uid' });
  1708. });
  1709. it('should update panel datasource props to refs for default data source', () => {
  1710. expect(model.panels[1].datasource).toEqual({ type: 'prometheus', uid: 'prom2-uid' });
  1711. });
  1712. it('should update panel datasource props to refs for mixed data source', () => {
  1713. expect(model.panels[2].datasource).toEqual({ type: 'mixed', uid: MIXED_DATASOURCE_NAME });
  1714. });
  1715. it('should update target datasource props to refs', () => {
  1716. expect(model.panels[2].targets[0].datasource).toEqual({ type: 'prometheus', uid: 'prom-uid' });
  1717. });
  1718. it('should update datasources in panels collapsed rows', () => {
  1719. expect(model.panels[3].panels?.[0].datasource).toEqual({ type: 'prometheus', uid: 'prom-uid' });
  1720. });
  1721. });
  1722. describe('when fixing query and panel data source refs out of sync due to default data source change', () => {
  1723. let model: DashboardModel;
  1724. beforeEach(() => {
  1725. model = new DashboardModel({
  1726. templating: {
  1727. list: [],
  1728. },
  1729. panels: [
  1730. {
  1731. id: 2,
  1732. datasource: null,
  1733. targets: [
  1734. {
  1735. datasource: 'prom-not-default',
  1736. },
  1737. ],
  1738. },
  1739. ],
  1740. });
  1741. });
  1742. it('should use data source on query level as source of truth', () => {
  1743. expect(model.panels[0].targets[0]?.datasource?.uid).toEqual('prom-not-default-uid');
  1744. expect(model.panels[0].datasource?.uid).toEqual('prom-not-default-uid');
  1745. });
  1746. });
  1747. describe('when migrating time series axis visibility', () => {
  1748. test('preserves x axis visibility', () => {
  1749. const model = new DashboardModel({
  1750. panels: [
  1751. {
  1752. type: 'timeseries',
  1753. fieldConfig: {
  1754. defaults: {
  1755. custom: {
  1756. axisPlacement: 'hidden',
  1757. },
  1758. },
  1759. overrides: [],
  1760. },
  1761. },
  1762. ],
  1763. });
  1764. expect(model.panels[0].fieldConfig.overrides).toMatchInlineSnapshot(`
  1765. Array [
  1766. Object {
  1767. "matcher": Object {
  1768. "id": "byType",
  1769. "options": "time",
  1770. },
  1771. "properties": Array [
  1772. Object {
  1773. "id": "custom.axisPlacement",
  1774. "value": "auto",
  1775. },
  1776. ],
  1777. },
  1778. ]
  1779. `);
  1780. });
  1781. });
  1782. describe('when migrating default (null) datasource', () => {
  1783. let model: DashboardModel;
  1784. beforeEach(() => {
  1785. model = new DashboardModel({
  1786. templating: {
  1787. list: [
  1788. {
  1789. type: 'query',
  1790. name: 'var',
  1791. options: [{ text: 'A', value: 'A' }],
  1792. refresh: 0,
  1793. datasource: null,
  1794. },
  1795. ],
  1796. },
  1797. annotations: {
  1798. list: [
  1799. {
  1800. datasource: null,
  1801. },
  1802. {
  1803. datasource: 'prom',
  1804. },
  1805. ],
  1806. },
  1807. panels: [
  1808. {
  1809. id: 2,
  1810. datasource: null,
  1811. targets: [
  1812. {
  1813. datasource: null,
  1814. },
  1815. ],
  1816. },
  1817. {
  1818. id: 3,
  1819. targets: [
  1820. {
  1821. refId: 'A',
  1822. },
  1823. ],
  1824. },
  1825. ],
  1826. schemaVersion: 35,
  1827. });
  1828. });
  1829. it('should set data source to current default', () => {
  1830. expect(model.templating.list[0].datasource).toEqual({ type: 'prometheus', uid: 'prom2-uid' });
  1831. });
  1832. it('should migrate annotation null query to default ds', () => {
  1833. expect(model.annotations.list[1].datasource).toEqual({ type: 'prometheus', uid: 'prom2-uid' });
  1834. });
  1835. it('should migrate annotation query to refs', () => {
  1836. expect(model.annotations.list[2].datasource).toEqual({ type: 'prometheus', uid: 'prom-uid' });
  1837. });
  1838. it('should update panel datasource props to refs for named data source', () => {
  1839. expect(model.panels[0].datasource).toEqual({ type: 'prometheus', uid: 'prom2-uid' });
  1840. });
  1841. it('should update panel datasource props even when undefined', () => {
  1842. expect(model.panels[1].datasource).toEqual({ type: 'prometheus', uid: 'prom2-uid' });
  1843. });
  1844. it('should update target datasource props to refs', () => {
  1845. expect(model.panels[0].targets[0].datasource).toEqual({ type: 'prometheus', uid: 'prom2-uid' });
  1846. });
  1847. });
  1848. describe('when migrating default (null) datasource with panel with expressions queries', () => {
  1849. let model: DashboardModel;
  1850. beforeEach(() => {
  1851. model = new DashboardModel({
  1852. panels: [
  1853. {
  1854. id: 2,
  1855. targets: [
  1856. {
  1857. refId: 'A',
  1858. },
  1859. {
  1860. refId: 'B',
  1861. datasource: '__expr__',
  1862. },
  1863. ],
  1864. },
  1865. ],
  1866. schemaVersion: 30,
  1867. });
  1868. });
  1869. it('should update panel datasource props to default datasource', () => {
  1870. expect(model.panels[0].datasource).toEqual({ type: 'prometheus', uid: 'prom2-uid' });
  1871. });
  1872. it('should update target datasource props to default data source', () => {
  1873. expect(model.panels[0].targets[0].datasource).toEqual({ type: 'prometheus', uid: 'prom2-uid' });
  1874. });
  1875. });
  1876. });
  1877. function createRow(options: any, panelDescriptions: any[]) {
  1878. const PANEL_HEIGHT_STEP = GRID_CELL_HEIGHT + GRID_CELL_VMARGIN;
  1879. const { collapse, showTitle, title, repeat, repeatIteration } = options;
  1880. let { height } = options;
  1881. height = height * PANEL_HEIGHT_STEP;
  1882. const panels: any[] = [];
  1883. each(panelDescriptions, (panelDesc) => {
  1884. const panel = { span: panelDesc[0] };
  1885. if (panelDesc.length > 1) {
  1886. //@ts-ignore
  1887. panel['height'] = panelDesc[1] * PANEL_HEIGHT_STEP;
  1888. }
  1889. panels.push(panel);
  1890. });
  1891. const row = {
  1892. collapse,
  1893. height,
  1894. showTitle,
  1895. title,
  1896. panels,
  1897. repeat,
  1898. repeatIteration,
  1899. };
  1900. return row;
  1901. }
  1902. function getGridPositions(dashboard: DashboardModel) {
  1903. return map(dashboard.panels, (panel: PanelModel) => {
  1904. return panel.gridPos;
  1905. });
  1906. }