Export: Remove DS input when dashboard is imported with a lib panel that already exists (#69412)

This commit is contained in:
Juan Cabanas 2023-07-27 13:11:15 -03:00 committed by GitHub
parent 649cd08a19
commit 427714f8d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 878 additions and 81 deletions

View File

@ -2731,10 +2731,10 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "1"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"], [0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"], [0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"], [0, 0, 0, "Do not use any type assertions.", "4"],
[0, 0, 0, "Do not use any type assertions.", "5"], [0, 0, 0, "Unexpected any. Specify a different type.", "5"],
[0, 0, 0, "Unexpected any. Specify a different type.", "6"], [0, 0, 0, "Do not use any type assertions.", "6"],
[0, 0, 0, "Do not use any type assertions.", "7"], [0, 0, 0, "Unexpected any. Specify a different type.", "7"],
[0, 0, 0, "Unexpected any. Specify a different type.", "8"], [0, 0, 0, "Unexpected any. Specify a different type.", "8"],
[0, 0, 0, "Unexpected any. Specify a different type.", "9"], [0, 0, 0, "Unexpected any. Specify a different type.", "9"],
[0, 0, 0, "Unexpected any. Specify a different type.", "10"], [0, 0, 0, "Unexpected any. Specify a different type.", "10"],
@ -2746,8 +2746,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "16"], [0, 0, 0, "Unexpected any. Specify a different type.", "16"],
[0, 0, 0, "Unexpected any. Specify a different type.", "17"], [0, 0, 0, "Unexpected any. Specify a different type.", "17"],
[0, 0, 0, "Unexpected any. Specify a different type.", "18"], [0, 0, 0, "Unexpected any. Specify a different type.", "18"],
[0, 0, 0, "Unexpected any. Specify a different type.", "19"], [0, 0, 0, "Unexpected any. Specify a different type.", "19"]
[0, 0, 0, "Unexpected any. Specify a different type.", "20"]
], ],
"public/app/features/manage-dashboards/state/reducers.ts:5381": [ "public/app/features/manage-dashboards/state/reducers.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "0"],

View File

@ -56,7 +56,7 @@ func (l *LibraryElementService) createHandler(c *contextmodel.ReqContext) respon
cmd.FolderID = folder.ID cmd.FolderID = folder.ID
} }
} }
element, err := l.createLibraryElement(c.Req.Context(), c.SignedInUser, cmd) element, err := l.CreateElement(c.Req.Context(), c.SignedInUser, cmd)
if err != nil { if err != nil {
return toLibraryElementError(err, "Failed to create library element") return toLibraryElementError(err, "Failed to create library element")
} }

View File

@ -120,12 +120,22 @@ func (l *LibraryElementService) createLibraryElement(c context.Context, signedIn
return model.LibraryElementDTO{}, model.ErrLibraryElementUIDTooLong return model.LibraryElementDTO{}, model.ErrLibraryElementUIDTooLong
} }
} }
updatedModel := cmd.Model
var err error
if cmd.Kind == int64(model.PanelElement) {
updatedModel, err = l.addUidToLibraryPanel(cmd.Model, createUID)
if err != nil {
return model.LibraryElementDTO{}, err
}
}
element := model.LibraryElement{ element := model.LibraryElement{
OrgID: signedInUser.OrgID, OrgID: signedInUser.OrgID,
FolderID: cmd.FolderID, FolderID: cmd.FolderID,
UID: createUID, UID: createUID,
Name: cmd.Name, Name: cmd.Name,
Model: cmd.Model, Model: updatedModel,
Version: 1, Version: 1,
Kind: cmd.Kind, Kind: cmd.Kind,
@ -140,7 +150,7 @@ func (l *LibraryElementService) createLibraryElement(c context.Context, signedIn
return model.LibraryElementDTO{}, err return model.LibraryElementDTO{}, err
} }
err := l.SQLStore.WithTransactionalDbSession(c, func(session *db.Session) error { err = l.SQLStore.WithTransactionalDbSession(c, func(session *db.Session) error {
if err := l.requireEditPermissionsOnFolder(c, signedInUser, cmd.FolderID); err != nil { if err := l.requireEditPermissionsOnFolder(c, signedInUser, cmd.FolderID); err != nil {
return err return err
} }
@ -228,7 +238,7 @@ func (l *LibraryElementService) deleteLibraryElement(c context.Context, signedIn
} }
// getLibraryElements gets a Library Element where param == value // getLibraryElements gets a Library Element where param == value
func getLibraryElements(c context.Context, store db.DB, cfg *setting.Cfg, signedInUser *user.SignedInUser, params []Pair, features featuremgmt.FeatureToggles, cmd model.GetLibraryElementCommand) ([]model.LibraryElementDTO, error) { func (l *LibraryElementService) getLibraryElements(c context.Context, store db.DB, cfg *setting.Cfg, signedInUser *user.SignedInUser, params []Pair, features featuremgmt.FeatureToggles, cmd model.GetLibraryElementCommand) ([]model.LibraryElementDTO, error) {
libraryElements := make([]model.LibraryElementWithMeta, 0) libraryElements := make([]model.LibraryElementWithMeta, 0)
recursiveQueriesAreSupported, err := store.RecursiveQueriesAreSupported() recursiveQueriesAreSupported, err := store.RecursiveQueriesAreSupported()
@ -267,6 +277,14 @@ func getLibraryElements(c context.Context, store db.DB, cfg *setting.Cfg, signed
leDtos := make([]model.LibraryElementDTO, len(libraryElements)) leDtos := make([]model.LibraryElementDTO, len(libraryElements))
for i, libraryElement := range libraryElements { for i, libraryElement := range libraryElements {
var updatedModel json.RawMessage
if libraryElement.Kind == int64(model.PanelElement) {
updatedModel, err = l.addUidToLibraryPanel(libraryElement.Model, libraryElement.UID)
if err != nil {
return []model.LibraryElementDTO{}, err
}
}
leDtos[i] = model.LibraryElementDTO{ leDtos[i] = model.LibraryElementDTO{
ID: libraryElement.ID, ID: libraryElement.ID,
OrgID: libraryElement.OrgID, OrgID: libraryElement.OrgID,
@ -277,7 +295,7 @@ func getLibraryElements(c context.Context, store db.DB, cfg *setting.Cfg, signed
Kind: libraryElement.Kind, Kind: libraryElement.Kind,
Type: libraryElement.Type, Type: libraryElement.Type,
Description: libraryElement.Description, Description: libraryElement.Description,
Model: libraryElement.Model, Model: updatedModel,
Version: libraryElement.Version, Version: libraryElement.Version,
Meta: model.LibraryElementDTOMeta{ Meta: model.LibraryElementDTOMeta{
FolderName: libraryElement.FolderName, FolderName: libraryElement.FolderName,
@ -304,7 +322,7 @@ func getLibraryElements(c context.Context, store db.DB, cfg *setting.Cfg, signed
// getLibraryElementByUid gets a Library Element by uid. // getLibraryElementByUid gets a Library Element by uid.
func (l *LibraryElementService) getLibraryElementByUid(c context.Context, signedInUser *user.SignedInUser, cmd model.GetLibraryElementCommand) (model.LibraryElementDTO, error) { func (l *LibraryElementService) getLibraryElementByUid(c context.Context, signedInUser *user.SignedInUser, cmd model.GetLibraryElementCommand) (model.LibraryElementDTO, error) {
libraryElements, err := getLibraryElements(c, l.SQLStore, l.Cfg, signedInUser, []Pair{{key: "org_id", value: signedInUser.OrgID}, {key: "uid", value: cmd.UID}}, l.features, cmd) libraryElements, err := l.getLibraryElements(c, l.SQLStore, l.Cfg, signedInUser, []Pair{{key: "org_id", value: signedInUser.OrgID}, {key: "uid", value: cmd.UID}}, l.features, cmd)
if err != nil { if err != nil {
return model.LibraryElementDTO{}, err return model.LibraryElementDTO{}, err
} }
@ -317,7 +335,7 @@ func (l *LibraryElementService) getLibraryElementByUid(c context.Context, signed
// getLibraryElementByName gets a Library Element by name. // getLibraryElementByName gets a Library Element by name.
func (l *LibraryElementService) getLibraryElementsByName(c context.Context, signedInUser *user.SignedInUser, name string) ([]model.LibraryElementDTO, error) { func (l *LibraryElementService) getLibraryElementsByName(c context.Context, signedInUser *user.SignedInUser, name string) ([]model.LibraryElementDTO, error) {
return getLibraryElements(c, l.SQLStore, l.Cfg, signedInUser, []Pair{{"org_id", signedInUser.OrgID}, {"name", name}}, l.features, return l.getLibraryElements(c, l.SQLStore, l.Cfg, signedInUser, []Pair{{"org_id", signedInUser.OrgID}, {"name", name}}, l.features,
model.GetLibraryElementCommand{ model.GetLibraryElementCommand{
FolderName: dashboards.RootFolderName, FolderName: dashboards.RootFolderName,
}) })

View File

@ -2,6 +2,7 @@ package libraryelements
import ( import (
"context" "context"
"encoding/json"
"github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db"
@ -75,3 +76,24 @@ func (l *LibraryElementService) DisconnectElementsFromDashboard(c context.Contex
func (l *LibraryElementService) DeleteLibraryElementsInFolder(c context.Context, signedInUser *user.SignedInUser, folderUID string) error { func (l *LibraryElementService) DeleteLibraryElementsInFolder(c context.Context, signedInUser *user.SignedInUser, folderUID string) error {
return l.deleteLibraryElementsInFolderUID(c, signedInUser, folderUID) return l.deleteLibraryElementsInFolderUID(c, signedInUser, folderUID)
} }
func (l *LibraryElementService) addUidToLibraryPanel(model []byte, newUid string) (json.RawMessage, error) {
var modelMap map[string]interface{}
err := json.Unmarshal(model, &modelMap)
if err != nil {
return nil, err
}
if libraryPanel, ok := modelMap["libraryPanel"].(map[string]interface{}); ok {
if uid, ok := libraryPanel["uid"]; ok && uid == "" {
libraryPanel["uid"] = newUid
}
}
updatedModel, err := json.Marshal(modelMap)
if err != nil {
return nil, err
}
return updatedModel, nil
}

View File

@ -164,8 +164,8 @@ it('replaces datasource ref in library panel', async () => {
if ('error' in exported) { if ('error' in exported) {
throw new Error('error should not be returned when making exportable json'); throw new Error('error should not be returned when making exportable json');
} }
expect(exported.__elements['c46a6b49-de40-43b3-982c-1b5e1ec084a4'].model.datasource.uid).toBe('${DS_GFDB}'); expect(exported.__elements!['c46a6b49-de40-43b3-982c-1b5e1ec084a4'].model.datasource.uid).toBe('${DS_GFDB}');
expect(exported.__inputs[0].name).toBe('DS_GFDB'); expect(exported.__inputs![0].name).toBe('DS_GFDB');
}); });
it('If a panel queries has no datasource prop ignore it', async () => { it('If a panel queries has no datasource prop ignore it', async () => {

View File

@ -13,12 +13,21 @@ import { VariableOption, VariableRefresh } from '../../../variables/types';
import { DashboardModel } from '../../state/DashboardModel'; import { DashboardModel } from '../../state/DashboardModel';
import { GridPos } from '../../state/PanelModel'; import { GridPos } from '../../state/PanelModel';
interface Input { export interface InputUsage {
libraryPanels?: LibraryPanel[];
}
export interface LibraryPanel {
name: string;
uid: string;
}
export interface Input {
name: string; name: string;
type: string; type: string;
label: string; label: string;
value: any; value: any;
description: string; description: string;
usage?: InputUsage;
} }
interface Requires { interface Requires {
@ -30,20 +39,17 @@ interface Requires {
}; };
} }
interface ExternalDashboard { export interface ExternalDashboard {
__inputs: Input[]; __inputs?: Input[];
__elements: Record<string, LibraryElementExport>; __elements?: Record<string, LibraryElementExport>;
__requires: Array<Requires[string]>; __requires?: Array<Requires[string]>;
panels: Array<PanelModel | PanelWithExportableLibraryPanel>; panels: Array<PanelModel | PanelWithExportableLibraryPanel>;
} }
interface PanelWithExportableLibraryPanel { interface PanelWithExportableLibraryPanel {
gridPos: GridPos; gridPos: GridPos;
id: number; id: number;
libraryPanel: { libraryPanel: LibraryPanel;
name: string;
uid: string;
};
} }
function isExportableLibraryPanel(p: any): p is PanelWithExportableLibraryPanel { function isExportableLibraryPanel(p: any): p is PanelWithExportableLibraryPanel {
@ -58,6 +64,7 @@ interface DataSources {
type: string; type: string;
pluginId: string; pluginId: string;
pluginName: string; pluginName: string;
usage?: InputUsage;
}; };
} }
@ -132,7 +139,10 @@ export class DashboardExporter {
return; return;
} }
const refName = 'DS_' + ds.name.replace(' ', '_').toUpperCase(); const libraryPanel = obj.libraryPanel;
const libraryPanelSuffix = !!libraryPanel ? '-for-library-panel' : '';
let refName = 'DS_' + ds.name.replace(' ', '_').toUpperCase() + libraryPanelSuffix.toUpperCase();
datasources[refName] = { datasources[refName] = {
name: refName, name: refName,
label: ds.name, label: ds.name,
@ -140,8 +150,18 @@ export class DashboardExporter {
type: 'datasource', type: 'datasource',
pluginId: ds.meta?.id, pluginId: ds.meta?.id,
pluginName: ds.meta?.name, pluginName: ds.meta?.name,
usage: datasources[refName]?.usage,
}; };
if (!!libraryPanel) {
const libPanels = datasources[refName]?.usage?.libraryPanels || [];
libPanels.push({ name: libraryPanel.name, uid: libraryPanel.uid });
datasources[refName].usage = {
libraryPanels: libPanels,
};
}
obj.datasource = { type: ds.meta.id, uid: '${' + refName + '}' }; obj.datasource = { type: ds.meta.id, uid: '${' + refName + '}' };
}); });
}; };

View File

@ -118,6 +118,7 @@ export const ImportDashboardForm = ({
return ( return (
<Field <Field
label={input.label} label={input.label}
description={input.description}
key={dataSourceOption} key={dataSourceOption}
invalid={errors.dataSources && !!errors.dataSources[index]} invalid={errors.dataSources && !!errors.dataSources[index]}
error={errors.dataSources && errors.dataSources[index] && 'A data source is required'} error={errors.dataSources && errors.dataSources[index] && 'A data source is required'}

View File

@ -1,13 +1,23 @@
import { thunkTester } from 'test/core/thunk/thunkTester'; import { thunkTester } from 'test/core/thunk/thunkTester';
import { DataSourceInstanceSettings } from '@grafana/data'; import { DataSourceInstanceSettings, ThresholdsMode } from '@grafana/data';
import { BackendSrv, setBackendSrv } from '@grafana/runtime'; import { BackendSrv, setBackendSrv } from '@grafana/runtime';
import { defaultDashboard, FieldColorModeId } from '@grafana/schema';
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 { validateDashboardJson } from '../utils/validation';
import { importDashboard } from './actions'; import { getLibraryPanelInputs, importDashboard, processDashboard } from './actions';
import { DataSourceInput, ImportDashboardDTO, initialImportDashboardState, InputType } from './reducers'; import { DataSourceInput, ImportDashboardDTO, initialImportDashboardState, InputType } from './reducers';
jest.mock('app/features/library-panels/state/api');
const mocks = {
getLibraryPanel: jest.mocked(getLibraryPanel),
};
describe('importDashboard', () => { describe('importDashboard', () => {
it('Should send data source uid', async () => { it('Should send data source uid', async () => {
const form: ImportDashboardDTO = { const form: ImportDashboardDTO = {
@ -107,3 +117,643 @@ describe('validateDashboardJson', () => {
expect(validateDashboardJsonNotValid).toBe('Not valid JSON'); 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);
});
});

View File

@ -1,22 +1,28 @@
import { DataSourceInstanceSettings, locationUtil } from '@grafana/data'; import { DataSourceInstanceSettings, locationUtil } from '@grafana/data';
import { getDataSourceSrv, locationService, getBackendSrv, isFetchError } from '@grafana/runtime'; import { getBackendSrv, getDataSourceSrv, isFetchError, locationService } from '@grafana/runtime';
import { notifyApp } from 'app/core/actions'; import { notifyApp } from 'app/core/actions';
import { createErrorNotification } from 'app/core/copy/appNotification'; import { createErrorNotification } from 'app/core/copy/appNotification';
import { SaveDashboardCommand } from 'app/features/dashboard/components/SaveDashboard/types'; import { SaveDashboardCommand } from 'app/features/dashboard/components/SaveDashboard/types';
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
import { DashboardDTO, FolderInfo, PermissionLevelString, SearchQueryType, ThunkResult } from 'app/types'; import { DashboardDTO, FolderInfo, PermissionLevelString, SearchQueryType, ThunkResult } from 'app/types';
import { LibraryElementExport } from '../../dashboard/components/DashExportModal/DashboardExporter'; import {
Input,
InputUsage,
LibraryElementExport,
LibraryPanel,
} from '../../dashboard/components/DashExportModal/DashboardExporter';
import { getLibraryPanel } from '../../library-panels/state/api'; import { getLibraryPanel } from '../../library-panels/state/api';
import { LibraryElementDTO, LibraryElementKind } from '../../library-panels/types'; import { LibraryElementDTO, LibraryElementKind } from '../../library-panels/types';
import { DashboardSearchHit } from '../../search/types'; import { DashboardSearchHit } from '../../search/types';
import { DeleteDashboardResponse } from '../types'; import { DashboardJson, DeleteDashboardResponse } from '../types';
import { import {
clearDashboard, clearDashboard,
fetchDashboard, fetchDashboard,
fetchFailed, fetchFailed,
ImportDashboardDTO, ImportDashboardDTO,
ImportDashboardState,
InputType, InputType,
LibraryPanelInput, LibraryPanelInput,
LibraryPanelInputState, LibraryPanelInputState,
@ -31,9 +37,9 @@ export function fetchGcomDashboard(id: string): ThunkResult<void> {
try { try {
dispatch(fetchDashboard()); dispatch(fetchDashboard());
const dashboard = await getBackendSrv().get(`/api/gnet/dashboards/${id}`); const dashboard = await getBackendSrv().get(`/api/gnet/dashboards/${id}`);
dispatch(setGcomDashboard(dashboard)); await dispatch(processElements(dashboard.json));
dispatch(processInputs(dashboard.json)); await dispatch(processGcomDashboard(dashboard));
dispatch(processElements(dashboard.json)); dispatch(processInputs());
} catch (error) { } catch (error) {
dispatch(fetchFailed()); dispatch(fetchFailed());
if (isFetchError(error)) { if (isFetchError(error)) {
@ -45,17 +51,66 @@ export function fetchGcomDashboard(id: string): ThunkResult<void> {
export function importDashboardJson(dashboard: any): ThunkResult<void> { export function importDashboardJson(dashboard: any): ThunkResult<void> {
return async (dispatch) => { return async (dispatch) => {
dispatch(setJsonDashboard(dashboard)); await dispatch(processElements(dashboard));
dispatch(processInputs(dashboard)); await dispatch(processJsonDashboard(dashboard));
dispatch(processElements(dashboard)); dispatch(processInputs());
}; };
} }
function processInputs(dashboardJson: any): ThunkResult<void> { const getNewLibraryPanelsByInput = (input: Input, state: ImportDashboardState): LibraryPanel[] | undefined => {
return (dispatch) => { return input?.usage?.libraryPanels?.filter((usageLibPanel) =>
if (dashboardJson && dashboardJson.__inputs) { state.inputs.libraryPanels.some(
(libPanel) => libPanel.state !== LibraryPanelInputState.Exists && libPanel.model.uid === usageLibPanel.uid
)
);
};
export function processDashboard(dashboardJson: DashboardJson, state: ImportDashboardState): DashboardJson {
let inputs = dashboardJson.__inputs;
if (!!state.inputs.libraryPanels?.length) {
const filteredUsedInputs: Input[] = [];
dashboardJson.__inputs?.forEach((input: Input) => {
if (!input?.usage?.libraryPanels) {
filteredUsedInputs.push(input);
return;
}
const newLibraryPanels = getNewLibraryPanelsByInput(input, state);
input.usage = { libraryPanels: newLibraryPanels };
const isInputBeingUsedByANewLibraryPanel = !!newLibraryPanels?.length;
if (isInputBeingUsedByANewLibraryPanel) {
filteredUsedInputs.push(input);
}
});
inputs = filteredUsedInputs;
}
return { ...dashboardJson, __inputs: inputs };
}
function processGcomDashboard(dashboard: { json: DashboardJson }): ThunkResult<void> {
return (dispatch, getState) => {
const state = getState().importDashboard;
const dashboardJson = processDashboard(dashboard.json, state);
dispatch(setGcomDashboard({ ...dashboard, json: dashboardJson }));
};
}
function processJsonDashboard(dashboardJson: DashboardJson): ThunkResult<void> {
return (dispatch, getState) => {
const state = getState().importDashboard;
const dashboard = processDashboard(dashboardJson, state);
dispatch(setJsonDashboard(dashboard));
};
}
function processInputs(): ThunkResult<void> {
return (dispatch, getState) => {
const dashboard = getState().importDashboard.dashboard;
if (dashboard && dashboard.__inputs) {
const inputs: any[] = []; const inputs: any[] = [];
dashboardJson.__inputs.forEach((input: any) => { dashboard.__inputs.forEach((input: any) => {
const inputModel: any = { const inputModel: any = {
name: input.name, name: input.name,
label: input.label, label: input.label,
@ -66,6 +121,8 @@ function processInputs(dashboardJson: any): ThunkResult<void> {
options: [], options: [],
}; };
inputModel.description = getDataSourceDescription(input);
if (input.type === InputType.DataSource) { if (input.type === InputType.DataSource) {
getDataSourceOptions(input, inputModel); getDataSourceOptions(input, inputModel);
} else if (!inputModel.info) { } else if (!inputModel.info) {
@ -81,8 +138,16 @@ function processInputs(dashboardJson: any): ThunkResult<void> {
function processElements(dashboardJson?: { __elements?: Record<string, LibraryElementExport> }): ThunkResult<void> { function processElements(dashboardJson?: { __elements?: Record<string, LibraryElementExport> }): ThunkResult<void> {
return async function (dispatch) { return async function (dispatch) {
const libraryPanelInputs = await getLibraryPanelInputs(dashboardJson);
dispatch(setLibraryPanelInputs(libraryPanelInputs));
};
}
export async function getLibraryPanelInputs(dashboardJson?: {
__elements?: Record<string, LibraryElementExport>;
}): Promise<LibraryPanelInput[]> {
if (!dashboardJson || !dashboardJson.__elements) { if (!dashboardJson || !dashboardJson.__elements) {
return; return [];
} }
const libraryPanelInputs: LibraryPanelInput[] = []; const libraryPanelInputs: LibraryPanelInput[] = [];
@ -121,8 +186,7 @@ function processElements(dashboardJson?: { __elements?: Record<string, LibraryEl
libraryPanelInputs.push(input); libraryPanelInputs.push(input);
} }
dispatch(setLibraryPanelInputs(libraryPanelInputs)); return libraryPanelInputs;
};
} }
export function clearLoadedDashboard(): ThunkResult<void> { export function clearLoadedDashboard(): ThunkResult<void> {
@ -182,6 +246,22 @@ const getDataSourceOptions = (input: { pluginId: string; pluginName: string }, i
} }
}; };
const getDataSourceDescription = (input: { usage?: InputUsage }): string | undefined => {
if (!input.usage) {
return undefined;
}
if (input.usage.libraryPanels) {
const libPanelNames = input.usage.libraryPanels.reduce(
(acc: string, libPanel, index) => (index === 0 ? libPanel.name : `${acc}, ${libPanel.name}`),
''
);
return `List of affected library panels: ${libPanelNames}`;
}
return undefined;
};
export async function moveFolders(folderUIDs: string[], toFolder: FolderInfo) { export async function moveFolders(folderUIDs: string[], toFolder: FolderInfo) {
const result = { const result = {
totalCount: folderUIDs.length, totalCount: folderUIDs.length,

View File

@ -34,6 +34,7 @@ export enum LibraryPanelInputState {
export interface DashboardInput { export interface DashboardInput {
name: string; name: string;
label: string; label: string;
description?: string;
info: string; info: string;
value: string; value: string;
type: InputType; type: InputType;
@ -100,7 +101,7 @@ const importDashboardSlice = createSlice({
state.inputs = { state.inputs = {
dataSources: action.payload.filter((p) => p.type === InputType.DataSource), dataSources: action.payload.filter((p) => p.type === InputType.DataSource),
constants: action.payload.filter((p) => p.type === InputType.Constant), constants: action.payload.filter((p) => p.type === InputType.Constant),
libraryPanels: [], libraryPanels: state.inputs.libraryPanels || [],
}; };
}, },
setLibraryPanelInputs: (state: Draft<ImportDashboardState>, action: PayloadAction<LibraryPanelInput[]>) => { setLibraryPanelInputs: (state: Draft<ImportDashboardState>, action: PayloadAction<LibraryPanelInput[]>) => {

View File

@ -1,3 +1,7 @@
import { Dashboard } from '@grafana/schema/src/veneer/dashboard.types';
import { ExternalDashboard } from '../dashboard/components/DashExportModal/DashboardExporter';
export interface Snapshot { export interface Snapshot {
created: string; created: string;
expires: string; expires: string;
@ -36,3 +40,5 @@ export interface PublicDashboardListResponse {
export interface PublicDashboardListWithPagination extends PublicDashboardListWithPaginationResponse { export interface PublicDashboardListWithPagination extends PublicDashboardListWithPaginationResponse {
totalPages: number; totalPages: number;
} }
export type DashboardJson = ExternalDashboard & Omit<Dashboard, 'panels'>;