123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522 |
- import { ComponentClass } from 'react';
- import {
- DataLinkBuiltInVars,
- FieldConfigProperty,
- PanelData,
- PanelProps,
- standardEditorsRegistry,
- standardFieldConfigEditorRegistry,
- dateTime,
- TimeRange,
- } from '@grafana/data';
- import { setTemplateSrv } from '@grafana/runtime';
- import { queryBuilder } from 'app/features/variables/shared/testing/builders';
- import { mockStandardFieldConfigOptions } from '../../../../test/helpers/fieldConfig';
- import { getPanelPlugin } from '../../plugins/__mocks__/pluginMocks';
- import { PanelQueryRunner } from '../../query/state/PanelQueryRunner';
- import { TemplateSrv } from '../../templating/template_srv';
- import { variableAdapters } from '../../variables/adapters';
- import { createQueryVariableAdapter } from '../../variables/query/adapter';
- import { setTimeSrv } from '../services/TimeSrv';
- import { TimeOverrideResult } from '../utils/panel';
- import { PanelModel } from './PanelModel';
- standardFieldConfigEditorRegistry.setInit(() => mockStandardFieldConfigOptions());
- standardEditorsRegistry.setInit(() => mockStandardFieldConfigOptions());
- setTimeSrv({
- timeRangeForUrl: () => ({
- from: 1607687293000,
- to: 1607687293100,
- }),
- } as any);
- const getVariables = () => variablesMock;
- const getVariableWithName = (name: string) => variablesMock.filter((v) => v.name === name)[0];
- const getFilteredVariables = jest.fn();
- setTemplateSrv(
- new TemplateSrv({
- getVariables,
- getVariableWithName,
- getFilteredVariables,
- })
- );
- variableAdapters.setInit(() => [createQueryVariableAdapter()]);
- describe('PanelModel', () => {
- describe('when creating new panel model', () => {
- let model: any;
- let modelJson: any;
- let persistedOptionsMock;
- const tablePlugin = getPanelPlugin(
- {
- id: 'table',
- },
- null as unknown as ComponentClass<PanelProps>, // react
- {} // angular
- );
- tablePlugin.setPanelOptions((builder) => {
- builder.addBooleanSwitch({
- name: 'Show thresholds',
- path: 'showThresholds',
- defaultValue: true,
- description: '',
- });
- });
- tablePlugin.useFieldConfig({
- standardOptions: {
- [FieldConfigProperty.Unit]: {
- defaultValue: 'flop',
- },
- [FieldConfigProperty.Decimals]: {
- defaultValue: 2,
- },
- },
- useCustomConfig: (builder) => {
- builder.addBooleanSwitch({
- name: 'CustomProp',
- path: 'customProp',
- defaultValue: false,
- });
- },
- });
- beforeEach(() => {
- persistedOptionsMock = {
- fieldOptions: {
- thresholds: [
- {
- color: '#F2495C',
- index: 1,
- value: 50,
- },
- {
- color: '#73BF69',
- index: 0,
- value: null,
- },
- ],
- },
- arrayWith2Values: [{ name: 'changed to only one value' }],
- };
- modelJson = {
- type: 'table',
- maxDataPoints: 100,
- interval: '5m',
- showColumns: true,
- targets: [{ refId: 'A' }, { noRefId: true }],
- options: persistedOptionsMock,
- fieldConfig: {
- defaults: {
- unit: 'mpg',
- thresholds: {
- mode: 'absolute',
- steps: [
- { color: 'green', value: null },
- { color: 'red', value: 80 },
- ],
- },
- },
- overrides: [
- {
- matcher: {
- id: '1',
- options: {},
- },
- properties: [
- {
- id: 'thresholds',
- value: {
- mode: 'absolute',
- steps: [
- { color: 'green', value: null },
- { color: 'red', value: 80 },
- ],
- },
- },
- ],
- },
- ],
- },
- };
- model = new PanelModel(modelJson);
- model.pluginLoaded(tablePlugin);
- });
- it('should apply defaults', () => {
- expect(model.gridPos.h).toBe(3);
- });
- it('should apply option defaults', () => {
- expect(model.getOptions().showThresholds).toBeTruthy();
- });
- it('should change null thresholds to negative infinity', () => {
- expect(model.fieldConfig.defaults.thresholds.steps[0].value).toBe(-Infinity);
- expect(model.fieldConfig.overrides[0].properties[0].value.steps[0].value).toBe(-Infinity);
- });
- it('should apply option defaults but not override if array is changed', () => {
- expect(model.getOptions().arrayWith2Values.length).toBe(1);
- });
- it('should apply field config defaults', () => {
- // default unit is overriden by model
- expect(model.getFieldOverrideOptions().fieldConfig.defaults.unit).toBe('mpg');
- // default decimals are aplied
- expect(model.getFieldOverrideOptions().fieldConfig.defaults.decimals).toBe(2);
- });
- it('should set model props on instance', () => {
- expect(model.showColumns).toBe(true);
- });
- it('should add missing refIds', () => {
- expect(model.targets[1].refId).toBe('B');
- });
- it("shouldn't break panel with non-array targets", () => {
- modelJson.targets = {
- 0: { refId: 'A' },
- foo: { bar: 'baz' },
- };
- model = new PanelModel(modelJson);
- expect(model.targets[0].refId).toBe('A');
- });
- it('getSaveModel should remove defaults', () => {
- const saveModel = model.getSaveModel();
- expect(saveModel.gridPos).toBe(undefined);
- });
- it('getSaveModel should remove nonPersistedProperties', () => {
- const saveModel = model.getSaveModel();
- expect(saveModel.events).toBe(undefined);
- });
- describe('variables interpolation', () => {
- beforeEach(() => {
- model.scopedVars = {
- aaa: { value: 'AAA', text: 'upperA' },
- bbb: { value: 'BBB', text: 'upperB' },
- };
- });
- it('should interpolate variables', () => {
- const out = model.replaceVariables('hello $aaa');
- expect(out).toBe('hello AAA');
- });
- it('should interpolate $__url_time_range variable', () => {
- const out = model.replaceVariables(`/d/1?$${DataLinkBuiltInVars.keepTime}`);
- expect(out).toBe('/d/1?from=1607687293000&to=1607687293100');
- });
- it('should interpolate $__all_variables variable', () => {
- const out = model.replaceVariables(`/d/1?$${DataLinkBuiltInVars.includeVars}`);
- expect(out).toBe('/d/1?var-test1=val1&var-test2=val2&var-test3=Value%203&var-test4=A&var-test4=B');
- });
- it('should prefer the local variable value', () => {
- const extra = { aaa: { text: '???', value: 'XXX' } };
- const out = model.replaceVariables('hello $aaa and $bbb', extra);
- expect(out).toBe('hello XXX and BBB');
- });
- it('Can use request scoped vars', () => {
- model.getQueryRunner().getLastRequest = () => {
- return {
- scopedVars: {
- __interval: { text: '10m', value: '10m' },
- },
- };
- };
- const out = model.replaceVariables('hello $__interval');
- expect(out).toBe('hello 10m');
- });
- });
- describe('when changing panel type', () => {
- beforeEach(() => {
- const newPlugin = getPanelPlugin({ id: 'graph' });
- newPlugin.useFieldConfig({
- standardOptions: {
- [FieldConfigProperty.Color]: {
- settings: {
- byThresholdsSupport: true,
- },
- },
- },
- useCustomConfig: (builder) => {
- builder.addNumberInput({
- path: 'customProp',
- name: 'customProp',
- defaultValue: 100,
- });
- },
- });
- newPlugin.setPanelOptions((builder) => {
- builder.addBooleanSwitch({
- name: 'Show thresholds labels',
- path: 'showThresholdLabels',
- defaultValue: false,
- description: '',
- });
- });
- model.fieldConfig.defaults.decimals = 3;
- model.fieldConfig.defaults.custom = {
- customProp: true,
- };
- model.fieldConfig.overrides = [
- {
- matcher: { id: 'byName', options: 'D-series' },
- properties: [
- {
- id: 'custom.customProp',
- value: false,
- },
- {
- id: 'decimals',
- value: 0,
- },
- ],
- },
- ];
- model.changePlugin(newPlugin);
- model.alert = { id: 2 };
- });
- it('should keep maxDataPoints', () => {
- expect(model.maxDataPoints).toBe(100);
- });
- it('should keep interval', () => {
- expect(model.interval).toBe('5m');
- });
- it('should preseve standard field config', () => {
- expect(model.fieldConfig.defaults.decimals).toEqual(3);
- });
- it('should clear custom field config and apply new defaults', () => {
- expect(model.fieldConfig.defaults.custom).toEqual({
- customProp: 100,
- });
- });
- it('should remove overrides with custom props', () => {
- expect(model.fieldConfig.overrides.length).toEqual(1);
- expect(model.fieldConfig.overrides[0].properties[0].id).toEqual('decimals');
- });
- it('should apply next panel option defaults', () => {
- expect(model.getOptions().showThresholdLabels).toBeFalsy();
- expect(model.getOptions().showThresholds).toBeUndefined();
- });
- it('should remove table properties but keep core props', () => {
- expect(model.showColumns).toBe(undefined);
- });
- it('should restore table properties when changing back', () => {
- model.changePlugin(tablePlugin);
- expect(model.showColumns).toBe(true);
- });
- it('should restore custom field config to what it was and preserve standard options', () => {
- model.changePlugin(tablePlugin);
- expect(model.fieldConfig.defaults.custom.customProp).toBe(true);
- });
- it('should remove alert rule when changing type that does not support it', () => {
- model.changePlugin(getPanelPlugin({ id: 'table' }));
- expect(model.alert).toBe(undefined);
- });
- });
- describe('when changing to react panel from angular panel', () => {
- let panelQueryRunner: any;
- const onPanelTypeChanged = jest.fn();
- const reactPlugin = getPanelPlugin({ id: 'react' }).setPanelChangeHandler(onPanelTypeChanged as any);
- beforeEach(() => {
- model.changePlugin(reactPlugin);
- panelQueryRunner = model.getQueryRunner();
- });
- it('should call react onPanelTypeChanged', () => {
- expect(onPanelTypeChanged.mock.calls.length).toBe(1);
- expect(onPanelTypeChanged.mock.calls[0][1]).toBe('table');
- expect(onPanelTypeChanged.mock.calls[0][2].angular).toBeDefined();
- });
- it('getQueryRunner() should return same instance after changing to another react panel', () => {
- model.changePlugin(getPanelPlugin({ id: 'react2' }));
- const sameQueryRunner = model.getQueryRunner();
- expect(panelQueryRunner).toBe(sameQueryRunner);
- });
- });
- describe('variables interpolation', () => {
- let panelQueryRunner: any;
- const onPanelTypeChanged = jest.fn();
- const reactPlugin = getPanelPlugin({ id: 'react' }).setPanelChangeHandler(onPanelTypeChanged as any);
- beforeEach(() => {
- model.changePlugin(reactPlugin);
- panelQueryRunner = model.getQueryRunner();
- });
- it('should call react onPanelTypeChanged', () => {
- expect(onPanelTypeChanged.mock.calls.length).toBe(1);
- expect(onPanelTypeChanged.mock.calls[0][1]).toBe('table');
- expect(onPanelTypeChanged.mock.calls[0][2].angular).toBeDefined();
- });
- it('getQueryRunner() should return same instance after changing to another react panel', () => {
- model.changePlugin(getPanelPlugin({ id: 'react2' }));
- const sameQueryRunner = model.getQueryRunner();
- expect(panelQueryRunner).toBe(sameQueryRunner);
- });
- });
- describe('restoreModel', () => {
- it('Should clean state and set properties from model', () => {
- model.restoreModel({
- title: 'New title',
- options: { new: true },
- });
- expect(model.title).toBe('New title');
- expect(model.options.new).toBe(true);
- });
- it('Should delete properties that are now gone on new model', () => {
- model.someProperty = 'value';
- model.restoreModel({
- title: 'New title',
- options: {},
- });
- expect(model.someProperty).toBeUndefined();
- });
- it('Should remove old angular panel specific props', () => {
- model.axes = [{ prop: 1 }];
- model.thresholds = [];
- model.restoreModel({
- title: 'New title',
- options: {},
- });
- expect(model.axes).toBeUndefined();
- expect(model.thresholds).toBeUndefined();
- });
- it('Should be able to set defaults back to default', () => {
- model.transparent = true;
- model.restoreModel({});
- expect(model.transparent).toBe(false);
- });
- });
- describe('updateGridPos', () => {
- it('Should not have changes if no change', () => {
- model.gridPos = { w: 1, h: 1, x: 1, y: 2 };
- model.updateGridPos({ w: 1, h: 1, x: 1, y: 2 });
- expect(model.hasChanged).toBe(false);
- });
- it('Should have changes if gridPos is different', () => {
- model.gridPos = { w: 1, h: 1, x: 1, y: 2 };
- model.updateGridPos({ w: 10, h: 1, x: 1, y: 2 });
- expect(model.hasChanged).toBe(true);
- });
- it('Should not have changes if not manually updated', () => {
- model.gridPos = { w: 1, h: 1, x: 1, y: 2 };
- model.updateGridPos({ w: 10, h: 1, x: 1, y: 2 }, false);
- expect(model.hasChanged).toBe(false);
- });
- });
- describe('destroy', () => {
- it('Should still preserve last query result', () => {
- model.getQueryRunner().useLastResultFrom({
- getLastResult: () => ({} as PanelData),
- } as PanelQueryRunner);
- model.destroy();
- expect(model.getQueryRunner().getLastResult()).toBeDefined();
- });
- });
- describe('getDisplayTitle', () => {
- it('when called then it should interpolate singe value variables in title', () => {
- const model = new PanelModel({
- title: 'Single value variable [[test3]] ${test3} ${test3:percentencode}',
- });
- const title = model.getDisplayTitle();
- expect(title).toEqual('Single value variable Value 3 Value 3 Value%203');
- });
- it('when called then it should interpolate multi value variables in title', () => {
- const model = new PanelModel({
- title: 'Multi value variable [[test4]] ${test4} ${test4:percentencode}',
- });
- const title = model.getDisplayTitle();
- expect(title).toEqual('Multi value variable A + B A + B %7BA%2CB%7D');
- });
- });
- describe('runAllPanelQueries', () => {
- it('when called then it should call all pending queries', () => {
- model.getQueryRunner = jest.fn().mockReturnValue({
- run: jest.fn(),
- });
- const dashboardId = 123;
- const dashboardTimezone = 'browser';
- const width = 860;
- const timeData = {
- timeInfo: '',
- timeRange: {
- from: dateTime([2019, 1, 11, 12, 0]),
- to: dateTime([2019, 1, 11, 18, 0]),
- raw: {
- from: 'now-6h',
- to: 'now',
- },
- } as TimeRange,
- } as TimeOverrideResult;
- model.runAllPanelQueries(dashboardId, dashboardTimezone, timeData, width);
- expect(model.getQueryRunner).toBeCalled();
- });
- });
- });
- });
- const variablesMock = [
- queryBuilder().withId('test1').withName('test1').withCurrent('val1').build(),
- queryBuilder().withId('test2').withName('test2').withCurrent('val2').build(),
- queryBuilder().withId('test3').withName('test3').withCurrent('Value 3').build(),
- queryBuilder().withId('test4').withName('test4').withCurrent(['A', 'B']).build(),
- ];
|