The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/features/manage-dashboards/state/actions.test.ts

757 lines
21 KiB

import { thunkTester } from 'test/core/thunk/thunkTester';
import { DataSourceInstanceSettings, ThresholdsMode } from '@grafana/data';
import { defaultDashboard, FieldColorModeId } from '@grafana/schema';
import { browseDashboardsAPI } from 'app/features/browse-dashboards/api/browseDashboardsAPI';
import { getLibraryPanel } from 'app/features/library-panels/state/api';
import { PanelModel } from '../../dashboard/state';
import { LibraryElementDTO } from '../../library-panels/types';
import { DashboardJson } from '../types';
import { validateDashboardJson } from '../utils/validation';
import { getLibraryPanelInputs, importDashboard, processDashboard } from './actions';
import { DataSourceInput, ImportDashboardDTO, initialImportDashboardState, InputType } from './reducers';
jest.mock('app/features/library-panels/state/api');
const mocks = {
getLibraryPanel: jest.mocked(getLibraryPanel),
};
describe('importDashboard', () => {
it('Should send data source uid', async () => {
// note: the actual action returned is more complicated
// but we don't really care about the return type in this test
// we're only testing that the correct data is passed to initiate
const mockAction = jest.fn().mockImplementation(() => ({
type: 'foo',
}));
const importDashboardRtkQueryMock = jest
.spyOn(browseDashboardsAPI.endpoints.importDashboard, 'initiate')
.mockImplementation(mockAction);
const form: ImportDashboardDTO = {
title: 'Asda',
uid: '12',
gnetId: 'asd',
constants: [],
dataSources: [
{
id: 1,
uid: 'ds-uid',
name: 'ds-name',
type: 'prometheus',
} as DataSourceInstanceSettings,
],
elements: [],
folder: {
uid: '5v6e5VH4z',
title: 'title',
},
};
await thunkTester({
importDashboard: {
...initialImportDashboardState,
inputs: {
dataSources: [
{
name: 'ds-name',
pluginId: 'prometheus',
type: InputType.DataSource,
},
] as DataSourceInput[],
constants: [],
libraryPanels: [],
},
},
})
.givenThunk(importDashboard)
.whenThunkIsDispatched(form);
expect(importDashboardRtkQueryMock).toHaveBeenCalledWith({
dashboard: {
title: 'Asda',
uid: '12',
},
folderUid: '5v6e5VH4z',
inputs: [
{
name: 'ds-name',
pluginId: 'prometheus',
type: 'datasource',
value: 'ds-uid',
},
],
overwrite: true,
});
});
});
describe('validateDashboardJson', () => {
it('Should return true if correct json', async () => {
const jsonImportCorrectFormat = '{"title": "Correct Format", "tags": ["tag1", "tag2"], "schemaVersion": 36}';
const validateDashboardJsonCorrectFormat = await validateDashboardJson(jsonImportCorrectFormat);
expect(validateDashboardJsonCorrectFormat).toBe(true);
});
it('Should not return true if nested tags', async () => {
const jsonImportNestedTags =
'{"title": "Nested tags","tags": ["tag1", "tag2", ["nestedTag1", "nestedTag2"]],"schemaVersion": 36}';
const validateDashboardJsonNestedTags = await validateDashboardJson(jsonImportNestedTags);
expect(validateDashboardJsonNestedTags).toBe('tags expected array of strings');
});
it('Should not return true if not an array', async () => {
const jsonImportNotArray = '{"title": "Not Array","tags": "tag1","schemaVersion":36}';
const validateDashboardJsonNotArray = await validateDashboardJson(jsonImportNotArray);
expect(validateDashboardJsonNotArray).toBe('tags expected array');
});
it('Should not return true if not an array and is blank string', async () => {
const jsonImportEmptyTags = '{"schemaVersion": 36,"tags": "", "title": "Empty Tags"}';
const validateDashboardJsonEmptyTags = await validateDashboardJson(jsonImportEmptyTags);
expect(validateDashboardJsonEmptyTags).toBe('tags expected array');
});
it('Should not return true if not valid JSON', async () => {
const jsonImportInvalidJson = '{"schemaVersion": 36,"tags": {"tag", "nested tag"}, "title": "Nested lists"}';
const validateDashboardJsonNotValid = await validateDashboardJson(jsonImportInvalidJson);
expect(validateDashboardJsonNotValid).toBe('Not valid JSON');
});
});
describe('processDashboard', () => {
const panel = new PanelModel({
datasource: {
type: 'mysql',
uid: '${DS_GDEV-MYSQL}',
},
});
const panelWithLibPanel = {
gridPos: {
h: 8,
w: 12,
x: 0,
y: 8,
},
id: 3,
libraryPanel: {
uid: 'a0379b21-fa20-4313-bf12-d7fd7ceb6f90',
name: 'another prom lib panel',
},
};
const libPanel = {
'a0379b21-fa20-4313-bf12-d7fd7ceb6f90': {
name: 'another prom lib panel',
uid: 'a0379b21-fa20-4313-bf12-d7fd7ceb6f90',
kind: 1,
model: {
datasource: {
type: 'prometheus',
uid: '${DS_GDEV-PROMETHEUS-FOR-LIBRARY-PANEL}',
},
description: '',
fieldConfig: {
defaults: {
color: {
mode: 'palette-classic',
},
custom: {
axisCenteredZero: false,
axisColorMode: 'text',
axisLabel: '',
axisPlacement: 'auto',
barAlignment: 0,
drawStyle: 'line',
fillOpacity: 0,
gradientMode: 'none',
hideFrom: {
legend: false,
tooltip: false,
viz: false,
},
lineInterpolation: 'linear',
lineWidth: 1,
pointSize: 5,
scaleDistribution: {
type: 'linear',
},
showPoints: 'auto',
spanNulls: false,
stacking: {
group: 'A',
mode: 'none',
},
thresholdsStyle: {
mode: 'off',
},
},
mappings: [],
thresholds: {
mode: 'absolute',
steps: [
{
color: 'green',
value: null,
},
{
color: 'red',
value: 80,
},
],
},
},
overrides: [],
},
libraryPanel: {
name: 'another prom lib panel',
uid: 'a0379b21-fa20-4313-bf12-d7fd7ceb6f90',
},
options: {
legend: {
calcs: [],
displayMode: 'list',
placement: 'bottom',
showLegend: true,
},
tooltip: {
mode: 'single',
sort: 'none',
},
},
targets: [
{
datasource: {
type: 'prometheus',
uid: 'gdev-prometheus',
},
editorMode: 'builder',
expr: 'access_evaluation_duration_bucket',
instant: false,
range: true,
refId: 'A',
},
],
title: 'Panel Title',
type: 'timeseries',
},
},
};
const panelWithSecondLibPanel = {
gridPos: {
h: 8,
w: 12,
x: 0,
y: 16,
},
id: 1,
libraryPanel: {
uid: 'c46a6b49-de40-43b3-982c-1b5e1ec084a4',
name: 'Testing lib panel',
},
};
const secondLibPanel = {
'c46a6b49-de40-43b3-982c-1b5e1ec084a4': {
name: 'Testing lib panel',
uid: 'c46a6b49-de40-43b3-982c-1b5e1ec084a4',
kind: 1,
model: {
datasource: {
type: 'prometheus',
uid: '${DS_GDEV-PROMETHEUS-FOR-LIBRARY-PANEL}',
},
description: '',
fieldConfig: {
defaults: {
color: {
mode: 'palette-classic',
},
custom: {
axisCenteredZero: false,
axisColorMode: 'text',
axisLabel: '',
axisPlacement: 'auto',
barAlignment: 0,
drawStyle: 'line',
fillOpacity: 0,
gradientMode: 'none',
hideFrom: {
legend: false,
tooltip: false,
viz: false,
},
lineInterpolation: 'linear',
lineWidth: 1,
pointSize: 5,
scaleDistribution: {
type: 'linear',
},
showPoints: 'auto',
spanNulls: false,
stacking: {
group: 'A',
mode: 'none',
},
thresholdsStyle: {
mode: 'off',
},
},
mappings: [],
thresholds: {
mode: 'absolute',
steps: [
{
color: 'green',
value: null,
},
{
color: 'red',
value: 80,
},
],
},
},
overrides: [],
},
libraryPanel: {
name: 'Testing lib panel',
uid: 'c46a6b49-de40-43b3-982c-1b5e1ec084a4',
},
options: {
legend: {
calcs: [],
displayMode: 'list',
placement: 'bottom',
showLegend: true,
},
tooltip: {
mode: 'single',
sort: 'none',
},
},
targets: [
{
datasource: {
type: 'prometheus',
uid: 'gdev-prometheus',
},
editorMode: 'builder',
expr: 'access_evaluation_duration_count',
instant: false,
range: true,
refId: 'A',
},
],
title: 'Panel Title',
type: 'timeseries',
},
},
};
const importedJson: DashboardJson = {
...defaultDashboard,
__inputs: [
{
name: 'DS_GDEV-MYSQL',
label: 'gdev-mysql',
description: '',
type: 'datasource',
value: '',
},
{
name: 'DS_GDEV-PROMETHEUS-FOR-LIBRARY-PANEL',
label: 'gdev-prometheus',
description: '',
type: 'datasource',
value: '',
usage: {
libraryPanels: [
{
name: 'another prom lib panel',
uid: 'a0379b21-fa20-4313-bf12-d7fd7ceb6f90',
},
],
},
},
],
__elements: {
...libPanel,
},
__requires: [
{
type: 'grafana',
id: 'grafana',
name: 'Grafana',
version: '10.1.0-pre',
},
{
type: 'datasource',
id: 'mysql',
name: 'MySQL',
version: '1.0.0',
},
{
type: 'datasource',
id: 'prometheus',
name: 'Prometheus',
version: '1.0.0',
},
{
type: 'panel',
id: 'table',
name: 'Table',
version: '',
},
],
panels: [],
};
it("Should return 2 inputs, 1 for library panel because it's used for 2 panels", async () => {
mocks.getLibraryPanel.mockImplementation(() => {
throw { status: 404 };
});
const importDashboardState = initialImportDashboardState;
const dashboardJson: DashboardJson = {
...importedJson,
panels: [panel, panelWithLibPanel, panelWithLibPanel],
};
const libPanelInputs = await getLibraryPanelInputs(dashboardJson);
const newDashboardState = {
...importDashboardState,
inputs: {
...importDashboardState.inputs,
libraryPanels: libPanelInputs!,
},
};
const processedDashboard = processDashboard(dashboardJson, newDashboardState);
const dsInputsForLibPanels = processedDashboard.__inputs!.filter((input) => !!input.usage?.libraryPanels);
expect(processedDashboard.__inputs).toHaveLength(2);
expect(dsInputsForLibPanels).toHaveLength(1);
});
it('Should return 3 inputs, 2 for library panels', async () => {
mocks.getLibraryPanel.mockImplementation(() => {
throw { status: 404 };
});
const importDashboardState = initialImportDashboardState;
const dashboardJson: DashboardJson = {
...importedJson,
__inputs: [
{
name: 'DS_GDEV-MYSQL',
label: 'gdev-mysql',
description: '',
type: 'datasource',
value: '',
},
{
name: 'DS_GDEV-PROMETHEUS-FOR-LIBRARY-PANEL',
label: 'gdev-prometheus',
description: '',
type: 'datasource',
value: '',
usage: {
libraryPanels: [
{
name: 'another prom lib panel',
uid: 'a0379b21-fa20-4313-bf12-d7fd7ceb6f90',
},
],
},
},
{
name: 'DS_GDEV-MYSQL-FOR-LIBRARY-PANEL',
label: 'gdev-mysql-2',
description: '',
type: 'datasource',
value: '',
usage: {
libraryPanels: [
{
uid: 'c46a6b49-de40-43b3-982c-1b5e1ec084a4',
name: 'Testing lib panel',
},
],
},
},
],
__elements: {
...libPanel,
...secondLibPanel,
},
panels: [panel, panelWithLibPanel, panelWithSecondLibPanel],
};
const libPanelInputs = await getLibraryPanelInputs(dashboardJson);
const newDashboardState = {
...importDashboardState,
inputs: {
...importDashboardState.inputs,
libraryPanels: libPanelInputs!,
},
};
const processedDashboard = processDashboard(dashboardJson, newDashboardState);
const dsInputsForLibPanels = processedDashboard.__inputs!.filter((input) => !!input.usage?.libraryPanels);
expect(processedDashboard.__inputs).toHaveLength(3);
expect(dsInputsForLibPanels).toHaveLength(2);
});
it('Should return 1 input, since library panels already exist in the instance', async () => {
const getLibPanelFirstRS: LibraryElementDTO = {
folderUid: '',
uid: 'a0379b21-fa20-4313-bf12-d7fd7ceb6f90',
name: 'another prom lib panel',
type: 'timeseries',
description: '',
model: {
transparent: false,
transformations: [],
datasource: {
type: 'prometheus',
uid: 'gdev-prometheus',
},
description: '',
fieldConfig: {
defaults: {
color: {
mode: FieldColorModeId.PaletteClassic,
},
custom: {
axisCenteredZero: false,
axisColorMode: 'text',
axisLabel: '',
axisPlacement: 'auto',
barAlignment: 0,
drawStyle: 'line',
fillOpacity: 0,
gradientMode: 'none',
hideFrom: {
legend: false,
tooltip: false,
viz: false,
},
lineInterpolation: 'linear',
lineWidth: 1,
pointSize: 5,
scaleDistribution: {
type: 'linear',
},
showPoints: 'auto',
spanNulls: false,
stacking: {
group: 'A',
mode: 'none',
},
thresholdsStyle: {
mode: 'off',
},
},
mappings: [],
thresholds: {
mode: ThresholdsMode.Absolute,
steps: [
{
color: 'green',
value: null,
},
{
color: 'red',
value: 80,
},
],
},
},
overrides: [],
},
options: {
legend: {
calcs: [],
displayMode: 'list',
placement: 'bottom',
showLegend: true,
},
tooltip: {
mode: 'single',
sort: 'none',
},
},
targets: [
{
datasource: {
type: 'prometheus',
uid: 'gdev-prometheus',
},
editorMode: 'builder',
expr: 'access_evaluation_duration_bucket',
instant: false,
range: true,
refId: 'A',
},
],
title: 'Panel Title',
type: 'timeseries',
},
version: 1,
};
const getLibPanelSecondRS: LibraryElementDTO = {
folderUid: '',
uid: 'c46a6b49-de40-43b3-982c-1b5e1ec084a4',
name: 'Testing lib panel',
type: 'timeseries',
description: '',
model: {
transparent: false,
transformations: [],
datasource: {
type: 'prometheus',
uid: 'gdev-prometheus',
},
description: '',
fieldConfig: {
defaults: {
color: {
mode: FieldColorModeId.PaletteClassic,
},
custom: {
axisCenteredZero: false,
axisColorMode: 'text',
axisLabel: '',
axisPlacement: 'auto',
barAlignment: 0,
drawStyle: 'line',
fillOpacity: 0,
gradientMode: 'none',
hideFrom: {
legend: false,
tooltip: false,
viz: false,
},
lineInterpolation: 'linear',
lineWidth: 1,
pointSize: 5,
scaleDistribution: {
type: 'linear',
},
showPoints: 'auto',
spanNulls: false,
stacking: {
group: 'A',
mode: 'none',
},
thresholdsStyle: {
mode: 'off',
},
},
mappings: [],
thresholds: {
mode: ThresholdsMode.Absolute,
steps: [
{
color: 'green',
value: null,
},
{
color: 'red',
value: 80,
},
],
},
},
overrides: [],
},
options: {
legend: {
calcs: [],
displayMode: 'list',
placement: 'bottom',
showLegend: true,
},
tooltip: {
mode: 'single',
sort: 'none',
},
},
targets: [
{
datasource: {
type: 'prometheus',
uid: 'gdev-prometheus',
},
editorMode: 'builder',
expr: 'access_evaluation_duration_count',
instant: false,
range: true,
refId: 'A',
},
],
title: 'Panel Title',
type: 'timeseries',
},
version: 1,
};
mocks.getLibraryPanel
.mockReturnValueOnce(Promise.resolve(getLibPanelFirstRS))
.mockReturnValueOnce(Promise.resolve(getLibPanelSecondRS));
const importDashboardState = initialImportDashboardState;
const dashboardJson: DashboardJson = {
...importedJson,
__inputs: [
{
name: 'DS_GDEV-MYSQL',
label: 'gdev-mysql',
description: '',
type: 'datasource',
value: '',
},
{
name: 'DS_GDEV-PROMETHEUS-FOR-LIBRARY-PANEL',
label: 'gdev-prometheus',
description: '',
type: 'datasource',
value: '',
usage: {
libraryPanels: [
{
name: 'another prom lib panel',
uid: 'a0379b21-fa20-4313-bf12-d7fd7ceb6f90',
},
],
},
},
{
name: 'DS_GDEV-MYSQL-FOR-LIBRARY-PANEL',
label: 'gdev-mysql-2',
description: '',
type: 'datasource',
value: '',
usage: {
libraryPanels: [
{
uid: 'c46a6b49-de40-43b3-982c-1b5e1ec084a4',
name: 'Testing lib panel',
},
],
},
},
],
__elements: {
...libPanel,
...secondLibPanel,
},
panels: [panel, panelWithLibPanel, panelWithSecondLibPanel],
};
const libPanelInputs = await getLibraryPanelInputs(dashboardJson);
const newDashboardState = {
...importDashboardState,
inputs: {
...importDashboardState.inputs,
libraryPanels: libPanelInputs!,
},
};
const processedDashboard = processDashboard(dashboardJson, newDashboardState);
const dsInputsForLibPanels = processedDashboard.__inputs!.filter((input) => !!input.usage?.libraryPanels);
expect(processedDashboard.__inputs).toHaveLength(1);
expect(dsInputsForLibPanels).toHaveLength(0);
});
});