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