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