mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
DashboardScenes: Use behavior for library panels (#90886)
* wip - refactor using libPanelBehavior * wip * wip * wip * migrate test * wip * nearly all tests done, except one commented which breaks * migrate last test * revert defaults.ini * fix tests * tests + fixes * fix * fix bug -- adding lib panel through CTA in empty dashboard didn't enter edit mode * show unsaved lib panel changes modal * fix height problems * fix * LibPanelBehavior: Fixes view panel and edit panel full page reload issues (alt fix) (#92850) * LibPanelsBehavior: Fixes view panel * Sort of working except panel edit * Got panel edit working * LibraryPanelsBehavior: Simpler fix * Cleanup * Remove more stuff * Update tests * fix tests * Work around timing issues * remove log * fix repeated panels not showing due to gridItem height not being set + translations * remove log that affects tests --------- Co-authored-by: Victor Marin <victor.marin@grafana.com> * fix translations * Fix save issue --------- Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
This commit is contained in:
parent
d3ceaf41c2
commit
4a4e3a4063
@ -16,9 +16,8 @@ import { GrafanaQueryType } from 'app/plugins/datasource/grafana/types';
|
||||
|
||||
import { DashboardGridItem } from '../../scene/DashboardGridItem';
|
||||
import { DashboardScene } from '../../scene/DashboardScene';
|
||||
import { LibraryVizPanel } from '../../scene/LibraryVizPanel';
|
||||
import { gridItemToPanel, vizPanelToPanel } from '../../serialization/transformSceneToSaveModel';
|
||||
import { getQueryRunnerFor } from '../../utils/utils';
|
||||
import { getQueryRunnerFor, isLibraryPanel } from '../../utils/utils';
|
||||
|
||||
import { Randomize, randomizeData } from './randomizer';
|
||||
|
||||
@ -64,11 +63,10 @@ export function getGithubMarkdown(panel: VizPanel, snapshot: string): string {
|
||||
|
||||
export async function getDebugDashboard(panel: VizPanel, rand: Randomize, timeRange: TimeRange) {
|
||||
let saveModel: ReturnType<typeof gridItemToPanel> = { type: '' };
|
||||
const isLibraryPanel = panel.parent instanceof LibraryVizPanel;
|
||||
const gridItem = (isLibraryPanel ? panel.parent.parent : panel.parent) as DashboardGridItem;
|
||||
const gridItem = panel.parent as DashboardGridItem;
|
||||
const scene = panel.getRoot() as DashboardScene;
|
||||
|
||||
if (isLibraryPanel) {
|
||||
if (isLibraryPanel(panel)) {
|
||||
saveModel = {
|
||||
...gridItemToPanel(gridItem),
|
||||
...vizPanelToPanel(panel),
|
||||
@ -78,7 +76,7 @@ export async function getDebugDashboard(panel: VizPanel, rand: Randomize, timeRa
|
||||
// we want the debug dashboard to include the panel with any changes that were made while
|
||||
// in panel edit mode.
|
||||
const sourcePanel = scene.state.editPanel.state.vizManager.state.sourcePanel.resolve();
|
||||
const dashGridItem = sourcePanel.parent instanceof LibraryVizPanel ? sourcePanel.parent.parent : sourcePanel.parent;
|
||||
const dashGridItem = sourcePanel.parent;
|
||||
if (dashGridItem instanceof DashboardGridItem) {
|
||||
saveModel = {
|
||||
...gridItemToPanel(dashGridItem),
|
||||
|
@ -5,6 +5,8 @@ import {
|
||||
getDefaultTimeRange,
|
||||
LoadingState,
|
||||
PanelData,
|
||||
PanelPlugin,
|
||||
PluginType,
|
||||
standardTransformersRegistry,
|
||||
toDataFrame,
|
||||
} from '@grafana/data';
|
||||
@ -16,7 +18,7 @@ import { getStandardTransformers } from 'app/features/transformers/standardTrans
|
||||
|
||||
import { DashboardGridItem } from '../scene/DashboardGridItem';
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
|
||||
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
|
||||
import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks';
|
||||
import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel';
|
||||
import { activateFullSceneTree } from '../utils/test-utils';
|
||||
@ -25,10 +27,33 @@ import { findVizPanelByKey } from '../utils/utils';
|
||||
import { InspectJsonTab } from './InspectJsonTab';
|
||||
|
||||
standardTransformersRegistry.setInit(getStandardTransformers);
|
||||
const panelPlugin: PanelPlugin = new PanelPlugin(() => null);
|
||||
panelPlugin.meta = {
|
||||
id: 'table',
|
||||
name: 'Table',
|
||||
sort: 1,
|
||||
type: PluginType.panel,
|
||||
info: {
|
||||
author: {
|
||||
name: 'name',
|
||||
},
|
||||
description: '',
|
||||
links: [],
|
||||
logos: {
|
||||
large: '',
|
||||
small: '',
|
||||
},
|
||||
screenshots: [],
|
||||
updated: '',
|
||||
version: '1.0.',
|
||||
},
|
||||
module: '',
|
||||
baseUrl: '',
|
||||
};
|
||||
|
||||
setPluginImportUtils({
|
||||
importPanelPlugin: (id: string) => Promise.resolve(getPanelPlugin({})),
|
||||
getPanelPluginFromCache: (id: string) => undefined,
|
||||
getPanelPluginFromCache: (id: string) => panelPlugin,
|
||||
});
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
@ -204,7 +229,29 @@ async function buildTestScene() {
|
||||
}
|
||||
|
||||
async function buildTestSceneWithLibraryPanel() {
|
||||
const panel = vizPanelToPanel(buildTestPanel());
|
||||
const libraryPanel = new VizPanel({
|
||||
title: 'Panel A',
|
||||
pluginId: 'table',
|
||||
key: 'panel-12',
|
||||
$behaviors: [new LibraryPanelBehavior({ title: 'LibraryPanel A title', name: 'LibraryPanel A', uid: '111' })],
|
||||
titleItems: [new VizPanelLinks({ menu: new VizPanelLinksMenu({}) })],
|
||||
$data: new SceneDataTransformer({
|
||||
transformations: [
|
||||
{
|
||||
id: 'reduce',
|
||||
options: {
|
||||
reducers: ['last'],
|
||||
},
|
||||
},
|
||||
],
|
||||
$data: new SceneQueryRunner({
|
||||
datasource: { uid: 'abcdef' },
|
||||
queries: [{ refId: 'A' }],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
const panel = vizPanelToPanel(libraryPanel.clone({ $behaviors: undefined }));
|
||||
|
||||
const libraryPanelState = {
|
||||
name: 'LibraryPanel A',
|
||||
@ -212,11 +259,20 @@ async function buildTestSceneWithLibraryPanel() {
|
||||
uid: '111',
|
||||
panelKey: 'panel-22',
|
||||
model: panel,
|
||||
type: 'table',
|
||||
version: 1,
|
||||
};
|
||||
|
||||
jest.spyOn(libpanels, 'getLibraryPanel').mockResolvedValue({ ...libraryPanelState, ...panel });
|
||||
const libraryPanel = new LibraryVizPanel(libraryPanelState);
|
||||
|
||||
const gridItem = new DashboardGridItem({
|
||||
key: 'griditem-1',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 10,
|
||||
height: 12,
|
||||
body: libraryPanel,
|
||||
});
|
||||
|
||||
const scene = new DashboardScene({
|
||||
title: 'hello',
|
||||
@ -225,16 +281,7 @@ async function buildTestSceneWithLibraryPanel() {
|
||||
canEdit: true,
|
||||
},
|
||||
body: new SceneGridLayout({
|
||||
children: [
|
||||
new DashboardGridItem({
|
||||
key: 'griditem-1',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 10,
|
||||
height: 12,
|
||||
body: libraryPanel,
|
||||
}),
|
||||
],
|
||||
children: [gridItem],
|
||||
}),
|
||||
});
|
||||
|
||||
@ -243,7 +290,7 @@ async function buildTestSceneWithLibraryPanel() {
|
||||
await new Promise((r) => setTimeout(r, 1));
|
||||
|
||||
const tab = new InspectJsonTab({
|
||||
panelRef: libraryPanel.state.panel!.getRef(),
|
||||
panelRef: libraryPanel.getRef(),
|
||||
onClose: jest.fn(),
|
||||
});
|
||||
|
||||
|
@ -27,10 +27,15 @@ import { reportPanelInspectInteraction } from 'app/features/search/page/reportin
|
||||
|
||||
import { VizPanelManager } from '../panel-edit/VizPanelManager';
|
||||
import { DashboardGridItem } from '../scene/DashboardGridItem';
|
||||
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
|
||||
import { buildGridItemForPanel } from '../serialization/transformSaveModelToScene';
|
||||
import { gridItemToPanel, vizPanelToPanel } from '../serialization/transformSceneToSaveModel';
|
||||
import { getDashboardSceneFor, getPanelIdForVizPanel, getQueryRunnerFor } from '../utils/utils';
|
||||
import {
|
||||
getDashboardSceneFor,
|
||||
getLibraryPanelBehavior,
|
||||
getPanelIdForVizPanel,
|
||||
getQueryRunnerFor,
|
||||
isLibraryPanel,
|
||||
} from '../utils/utils';
|
||||
|
||||
export type ShowContent = 'panel-json' | 'panel-data' | 'data-frames';
|
||||
|
||||
@ -142,7 +147,7 @@ export class InspectJsonTab extends SceneObjectBase<InspectJsonTabState> {
|
||||
const panel = this.state.panelRef.resolve();
|
||||
|
||||
// Library panels are not editable from the inspect
|
||||
if (panel.parent instanceof LibraryVizPanel) {
|
||||
if (isLibraryPanel(panel)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -206,11 +211,11 @@ function getJsonText(show: ShowContent, panel: VizPanel): string {
|
||||
case 'panel-json': {
|
||||
reportPanelInspectInteraction(InspectTab.JSON, 'panelData');
|
||||
|
||||
const isInspectingLibraryPanel = panel.parent instanceof LibraryVizPanel;
|
||||
const gridItem = isInspectingLibraryPanel ? panel.parent.parent : panel.parent;
|
||||
const isInspectingLibraryPanel = isLibraryPanel(panel);
|
||||
const gridItem = panel.parent;
|
||||
|
||||
if (isInspectingLibraryPanel) {
|
||||
objToStringify = libraryPanelChildToLegacyRepresentation(panel);
|
||||
objToStringify = libraryPanelToLegacyRepresentation(panel);
|
||||
break;
|
||||
}
|
||||
|
||||
@ -256,19 +261,20 @@ function getJsonText(show: ShowContent, panel: VizPanel): string {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param panel Must be child of a LibraryVizPanel that is in turn the child of a DashboardGridItem
|
||||
* @param panel Must hold a LibraryPanel behavior
|
||||
* @returns object representation of the legacy library panel structure.
|
||||
*/
|
||||
function libraryPanelChildToLegacyRepresentation(panel: VizPanel<{}, {}>) {
|
||||
if (!(panel.parent instanceof LibraryVizPanel)) {
|
||||
throw 'Panel not child of LibraryVizPanel';
|
||||
function libraryPanelToLegacyRepresentation(panel: VizPanel<{}, {}>) {
|
||||
if (!isLibraryPanel(panel)) {
|
||||
throw 'Panel not a library panel';
|
||||
}
|
||||
|
||||
if (!(panel.parent.parent instanceof DashboardGridItem)) {
|
||||
const gridItem = panel.parent;
|
||||
|
||||
if (!(gridItem instanceof DashboardGridItem)) {
|
||||
throw 'LibraryPanel not child of DashboardGridItem';
|
||||
}
|
||||
|
||||
const gridItem = panel.parent.parent;
|
||||
const gridPos = {
|
||||
x: gridItem.state.x || 0,
|
||||
y: gridItem.state.y || 0,
|
||||
@ -276,19 +282,26 @@ function libraryPanelChildToLegacyRepresentation(panel: VizPanel<{}, {}>) {
|
||||
w: gridItem.state.width || 0,
|
||||
};
|
||||
const libraryPanelObj = vizPanelToLibraryPanel(panel);
|
||||
const panelObj = vizPanelToPanel(panel, gridPos, false, gridItem);
|
||||
const panelObj = vizPanelToPanel(panel.clone({ $behaviors: undefined }), gridPos, false, gridItem);
|
||||
|
||||
return { libraryPanel: { ...libraryPanelObj }, ...panelObj };
|
||||
}
|
||||
|
||||
function vizPanelToLibraryPanel(panel: VizPanel): LibraryPanel {
|
||||
if (!(panel.parent instanceof LibraryVizPanel)) {
|
||||
throw new Error('Panel not a child of LibraryVizPanel');
|
||||
if (!isLibraryPanel(panel)) {
|
||||
throw new Error('Panel not a Library panel');
|
||||
}
|
||||
if (!panel.parent.state._loadedPanel) {
|
||||
|
||||
const libraryPanel = getLibraryPanelBehavior(panel);
|
||||
|
||||
if (!libraryPanel) {
|
||||
throw new Error('Library panel behavior not found');
|
||||
}
|
||||
|
||||
if (!libraryPanel.state._loadedPanel) {
|
||||
throw new Error('Library panel not loaded');
|
||||
}
|
||||
return panel.parent.state._loadedPanel;
|
||||
return libraryPanel.state._loadedPanel;
|
||||
}
|
||||
|
||||
function hasGridPosChanged(a: SceneGridItemStateLike, b: SceneGridItemStateLike) {
|
||||
|
@ -3,10 +3,10 @@ import { css } from '@emotion/css';
|
||||
import { GrafanaTheme2, dateTimeFormat } from '@grafana/data';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
|
||||
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
|
||||
|
||||
interface Props {
|
||||
libraryPanel: LibraryVizPanel;
|
||||
libraryPanel: LibraryPanelBehavior;
|
||||
}
|
||||
|
||||
export const LibraryVizPanelInfo = ({ libraryPanel }: Props) => {
|
||||
|
@ -4,7 +4,7 @@ import * as libAPI from 'app/features/library-panels/state/api';
|
||||
|
||||
import { DashboardGridItem } from '../scene/DashboardGridItem';
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
|
||||
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
|
||||
import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel';
|
||||
import { activateFullSceneTree } from '../utils/test-utils';
|
||||
|
||||
@ -135,20 +135,19 @@ describe('PanelEditor', () => {
|
||||
version: 1,
|
||||
};
|
||||
|
||||
const libraryPanel = new LibraryVizPanel({
|
||||
const libPanelBehavior = new LibraryPanelBehavior({
|
||||
isLoaded: true,
|
||||
title: libraryPanelModel.title,
|
||||
uid: libraryPanelModel.uid,
|
||||
name: libraryPanelModel.name,
|
||||
panelKey: panel.state.key!,
|
||||
panel: panel,
|
||||
_loadedPanel: libraryPanelModel,
|
||||
});
|
||||
const gridItem = new DashboardGridItem({ body: libraryPanel });
|
||||
|
||||
const apiCall = jest
|
||||
.spyOn(libAPI, 'updateLibraryVizPanel')
|
||||
.mockResolvedValue({ type: 'panel', ...libAPI.libraryVizPanelToSaveModel(libraryPanel), version: 2 });
|
||||
panel.setState({
|
||||
$behaviors: [libPanelBehavior],
|
||||
});
|
||||
|
||||
const gridItem = new DashboardGridItem({ body: panel });
|
||||
|
||||
const editScene = buildPanelEditScene(panel);
|
||||
const scene = new DashboardScene({
|
||||
@ -162,17 +161,23 @@ describe('PanelEditor', () => {
|
||||
activateFullSceneTree(scene);
|
||||
|
||||
editScene.state.vizManager.state.panel.setState({ title: 'changed title' });
|
||||
(editScene.state.vizManager.state.sourcePanel.resolve().parent as LibraryVizPanel).setState({
|
||||
(editScene.state.vizManager.state.panel.state.$behaviors![0] as LibraryPanelBehavior).setState({
|
||||
name: 'changed name',
|
||||
});
|
||||
|
||||
jest.spyOn(libAPI, 'saveLibPanel').mockImplementation(async (panel) => {
|
||||
const updatedPanel = { ...libAPI.libraryVizPanelToSaveModel(panel), version: 2 };
|
||||
|
||||
libPanelBehavior.setPanelFromLibPanel(updatedPanel);
|
||||
});
|
||||
|
||||
editScene.state.vizManager.commitChanges();
|
||||
|
||||
const calledWith = apiCall.mock.calls[0][0].state;
|
||||
expect(calledWith.panel?.state.title).toBe('changed title');
|
||||
expect(calledWith.name).toBe('changed name');
|
||||
|
||||
await new Promise(process.nextTick); // Wait for mock api to return and update the library panel
|
||||
expect((gridItem.state.body as LibraryVizPanel).state._loadedPanel?.version).toBe(2);
|
||||
expect(libPanelBehavior.state._loadedPanel?.version).toBe(2);
|
||||
expect(libPanelBehavior.state.name).toBe('changed name');
|
||||
expect(libPanelBehavior.state.title).toBe('changed title');
|
||||
expect((gridItem.state.body as VizPanel).state.title).toBe('changed title');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -5,7 +5,6 @@ import { config, locationService } from '@grafana/runtime';
|
||||
import { SceneObjectBase, SceneObjectState, VizPanel } from '@grafana/scenes';
|
||||
|
||||
import { DashboardGridItem } from '../scene/DashboardGridItem';
|
||||
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
|
||||
import { getDashboardSceneFor, getPanelIdForVizPanel } from '../utils/utils';
|
||||
|
||||
import { PanelDataPane } from './PanelDataPane/PanelDataPane';
|
||||
@ -93,6 +92,7 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
|
||||
}
|
||||
|
||||
public onDiscard = () => {
|
||||
this.state.vizManager.setState({ isDirty: false });
|
||||
this._discardChanges = true;
|
||||
locationService.partial({ editPanel: null });
|
||||
};
|
||||
@ -106,15 +106,7 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
|
||||
|
||||
const panelManager = this.state.vizManager;
|
||||
const sourcePanel = panelManager.state.sourcePanel.resolve();
|
||||
const sourcePanelParent = sourcePanel!.parent;
|
||||
const isLibraryPanel = sourcePanelParent instanceof LibraryVizPanel;
|
||||
|
||||
const gridItem = isLibraryPanel ? sourcePanelParent.parent : sourcePanelParent;
|
||||
|
||||
if (isLibraryPanel) {
|
||||
// Library panels handled separately
|
||||
return;
|
||||
}
|
||||
const gridItem = sourcePanel!.parent;
|
||||
|
||||
if (!(gridItem instanceof DashboardGridItem)) {
|
||||
console.error('Unsupported scene object type');
|
||||
@ -155,6 +147,7 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
|
||||
|
||||
public onConfirmSaveLibraryPanel = () => {
|
||||
this.state.vizManager.commitChanges();
|
||||
this.state.vizManager.setState({ isDirty: false });
|
||||
locationService.partial({ editPanel: null });
|
||||
};
|
||||
|
||||
|
@ -8,7 +8,7 @@ import { Button, ToolbarButton, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { NavToolbarActions } from '../scene/NavToolbarActions';
|
||||
import { UnlinkModal } from '../scene/UnlinkModal';
|
||||
import { getDashboardSceneFor, getLibraryPanel } from '../utils/utils';
|
||||
import { getDashboardSceneFor, getLibraryPanelBehavior } from '../utils/utils';
|
||||
|
||||
import { PanelEditor } from './PanelEditor';
|
||||
import { SaveLibraryVizPanelModal } from './SaveLibraryVizPanelModal';
|
||||
@ -68,7 +68,7 @@ function VizAndDataPane({ model }: SceneComponentProps<PanelEditor>) {
|
||||
const dashboard = getDashboardSceneFor(model);
|
||||
const { vizManager, dataPane, showLibraryPanelSaveModal, showLibraryPanelUnlinkModal } = model.useState();
|
||||
const { sourcePanel } = vizManager.useState();
|
||||
const libraryPanel = getLibraryPanel(sourcePanel.resolve());
|
||||
const libraryPanel = getLibraryPanelBehavior(sourcePanel.resolve());
|
||||
const { controls } = dashboard.useState();
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
|
@ -12,7 +12,7 @@ import { overrideRuleTooltipDescription } from 'app/features/dashboard/component
|
||||
|
||||
import { DashboardGridItem } from '../scene/DashboardGridItem';
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
|
||||
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
|
||||
import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel';
|
||||
import { activateFullSceneTree } from '../utils/test-utils';
|
||||
import * as utils from '../utils/utils';
|
||||
@ -171,17 +171,19 @@ describe('PanelOptions', () => {
|
||||
version: 1,
|
||||
};
|
||||
|
||||
const libraryPanel = new LibraryVizPanel({
|
||||
const libraryPanel = new LibraryPanelBehavior({
|
||||
isLoaded: true,
|
||||
title: libraryPanelModel.title,
|
||||
uid: libraryPanelModel.uid,
|
||||
name: libraryPanelModel.name,
|
||||
panelKey: panel.state.key!,
|
||||
panel: panel,
|
||||
_loadedPanel: libraryPanelModel,
|
||||
});
|
||||
|
||||
new DashboardGridItem({ body: libraryPanel });
|
||||
panel.setState({
|
||||
$behaviors: [libraryPanel],
|
||||
});
|
||||
|
||||
new DashboardGridItem({ body: panel });
|
||||
|
||||
const { renderResult, vizManager } = setup({ panel: panel });
|
||||
|
||||
@ -191,7 +193,7 @@ describe('PanelOptions', () => {
|
||||
fireEvent.blur(input, { target: { value: 'new library panel name' } });
|
||||
});
|
||||
|
||||
expect((vizManager.state.sourcePanel.resolve().parent as LibraryVizPanel).state.name).toBe(
|
||||
expect((vizManager.state.panel.state.$behaviors![0] as LibraryPanelBehavior).state.name).toBe(
|
||||
'new library panel name'
|
||||
);
|
||||
});
|
||||
|
@ -2,6 +2,7 @@ import { useMemo } from 'react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { PanelData } from '@grafana/data';
|
||||
import { VizPanel } from '@grafana/scenes';
|
||||
import { OptionFilter, renderSearchHits } from 'app/features/dashboard/components/PanelEditor/OptionsPaneOptions';
|
||||
import { getFieldOverrideCategories } from 'app/features/dashboard/components/PanelEditor/getFieldOverrideElements';
|
||||
import {
|
||||
@ -9,7 +10,8 @@ import {
|
||||
getVisualizationOptions2,
|
||||
} from 'app/features/dashboard/components/PanelEditor/getVisualizationOptions';
|
||||
|
||||
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
|
||||
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
|
||||
import { getLibraryPanelBehavior, isLibraryPanel } from '../utils/utils';
|
||||
|
||||
import { VizPanelManager } from './VizPanelManager';
|
||||
import { getPanelFrameCategory2 } from './getPanelFrameOptions';
|
||||
@ -22,8 +24,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const PanelOptions = React.memo<Props>(({ vizManager, searchQuery, listMode, data }) => {
|
||||
const { panel, sourcePanel, repeat } = vizManager.useState();
|
||||
const parent = sourcePanel.resolve().parent;
|
||||
const { panel, repeat } = vizManager.useState();
|
||||
const { options, fieldConfig, _pluginInstanceState } = panel.useState();
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@ -49,11 +50,17 @@ export const PanelOptions = React.memo<Props>(({ vizManager, searchQuery, listMo
|
||||
}, [panel, options, fieldConfig, _pluginInstanceState]);
|
||||
|
||||
const libraryPanelOptions = useMemo(() => {
|
||||
if (parent instanceof LibraryVizPanel) {
|
||||
return getLibraryVizPanelOptionsCategory(parent);
|
||||
if (panel instanceof VizPanel && isLibraryPanel(panel)) {
|
||||
const behavior = getLibraryPanelBehavior(panel);
|
||||
|
||||
if (!(behavior instanceof LibraryPanelBehavior)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return getLibraryVizPanelOptionsCategory(behavior);
|
||||
}
|
||||
return;
|
||||
}, [parent]);
|
||||
}, [panel]);
|
||||
|
||||
const justOverrides = useMemo(
|
||||
() =>
|
||||
|
@ -5,10 +5,10 @@ import { Button, Icon, Input, Modal, useStyles2 } from '@grafana/ui';
|
||||
import { getConnectedDashboards } from 'app/features/library-panels/state/api';
|
||||
import { getModalStyles } from 'app/features/library-panels/styles';
|
||||
|
||||
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
|
||||
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
|
||||
|
||||
interface Props {
|
||||
libraryPanel: LibraryVizPanel;
|
||||
libraryPanel: LibraryPanelBehavior;
|
||||
isUnsavedPrompt?: boolean;
|
||||
onConfirm: () => void;
|
||||
onDismiss: () => void;
|
||||
|
@ -20,7 +20,7 @@ import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard';
|
||||
import { DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard/types';
|
||||
|
||||
import { DashboardGridItem } from '../scene/DashboardGridItem';
|
||||
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
|
||||
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
|
||||
import { PanelTimeRange, PanelTimeRangeState } from '../scene/PanelTimeRange';
|
||||
import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
|
||||
import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel';
|
||||
@ -286,28 +286,28 @@ describe('VizPanelManager', () => {
|
||||
version: 1,
|
||||
};
|
||||
|
||||
const libraryPanel = new LibraryVizPanel({
|
||||
const libPanelBehavior = new LibraryPanelBehavior({
|
||||
isLoaded: true,
|
||||
title: libraryPanelModel.title,
|
||||
uid: libraryPanelModel.uid,
|
||||
name: libraryPanelModel.name,
|
||||
panelKey: panel.state.key!,
|
||||
panel: panel,
|
||||
_loadedPanel: libraryPanelModel,
|
||||
});
|
||||
|
||||
new DashboardGridItem({ body: libraryPanel });
|
||||
panel.setState({
|
||||
$behaviors: [libPanelBehavior],
|
||||
});
|
||||
|
||||
new DashboardGridItem({ body: panel });
|
||||
|
||||
const panelManager = VizPanelManager.createFor(panel);
|
||||
|
||||
const apiCall = jest
|
||||
.spyOn(libAPI, 'updateLibraryVizPanel')
|
||||
.mockResolvedValue({ type: 'panel', ...libAPI.libraryVizPanelToSaveModel(libraryPanel) });
|
||||
const apiCall = jest.spyOn(libAPI, 'saveLibPanel');
|
||||
|
||||
panelManager.state.panel.setState({ title: 'new title' });
|
||||
panelManager.commitChanges();
|
||||
|
||||
expect(apiCall.mock.calls[0][0].state.panel?.state.title).toBe('new title');
|
||||
expect(apiCall.mock.calls[0][0].state.title).toBe('new title');
|
||||
});
|
||||
|
||||
it('unlinks library panel', () => {
|
||||
@ -325,23 +325,25 @@ describe('VizPanelManager', () => {
|
||||
version: 1,
|
||||
};
|
||||
|
||||
const libraryPanel = new LibraryVizPanel({
|
||||
const libPanelBehavior = new LibraryPanelBehavior({
|
||||
isLoaded: true,
|
||||
title: libraryPanelModel.title,
|
||||
uid: libraryPanelModel.uid,
|
||||
name: libraryPanelModel.name,
|
||||
panelKey: panel.state.key!,
|
||||
panel: panel,
|
||||
_loadedPanel: libraryPanelModel,
|
||||
});
|
||||
|
||||
const gridItem = new DashboardGridItem({ body: libraryPanel });
|
||||
panel.setState({
|
||||
$behaviors: [libPanelBehavior],
|
||||
});
|
||||
|
||||
new DashboardGridItem({ body: panel });
|
||||
|
||||
const panelManager = VizPanelManager.createFor(panel);
|
||||
panelManager.unlinkLibraryPanel();
|
||||
|
||||
const sourcePanel = panelManager.state.sourcePanel.resolve();
|
||||
expect(sourcePanel.parent?.state.key).toBe(gridItem.state.key);
|
||||
expect(sourcePanel.state.$behaviors).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -34,7 +34,7 @@ import { DataQuery, DataTransformerConfig, Panel } from '@grafana/schema';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { getLastUsedDatasourceFromStorage } from 'app/features/dashboard/utils/dashboard';
|
||||
import { storeLastUsedDataSourceInLocalStorage } from 'app/features/datasources/components/picker/utils';
|
||||
import { updateLibraryVizPanel } from 'app/features/library-panels/state/api';
|
||||
import { saveLibPanel } from 'app/features/library-panels/state/api';
|
||||
import { updateQueries } from 'app/features/query/state/updateQueries';
|
||||
import { GrafanaQuery } from 'app/plugins/datasource/grafana/types';
|
||||
import { QueryGroupOptions } from 'app/types';
|
||||
@ -42,10 +42,15 @@ import { QueryGroupOptions } from 'app/types';
|
||||
import { DashboardSceneChangeTracker } from '../saving/DashboardSceneChangeTracker';
|
||||
import { getPanelChanges } from '../saving/getDashboardChanges';
|
||||
import { DashboardGridItem, RepeatDirection } from '../scene/DashboardGridItem';
|
||||
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
|
||||
import { PanelTimeRange, PanelTimeRangeState } from '../scene/PanelTimeRange';
|
||||
import { gridItemToPanel, vizPanelToPanel } from '../serialization/transformSceneToSaveModel';
|
||||
import { getDashboardSceneFor, getMultiVariableValues, getPanelIdForVizPanel, getQueryRunnerFor } from '../utils/utils';
|
||||
import {
|
||||
getDashboardSceneFor,
|
||||
getMultiVariableValues,
|
||||
getPanelIdForVizPanel,
|
||||
getQueryRunnerFor,
|
||||
isLibraryPanel,
|
||||
} from '../utils/utils';
|
||||
|
||||
export interface VizPanelManagerState extends SceneObjectState {
|
||||
panel: VizPanel;
|
||||
@ -85,7 +90,7 @@ export class VizPanelManager extends SceneObjectBase<VizPanelManagerState> {
|
||||
public static createFor(sourcePanel: VizPanel) {
|
||||
let repeatOptions: Pick<VizPanelManagerState, 'repeat' | 'repeatDirection' | 'maxPerRow'> = {};
|
||||
|
||||
const gridItem = sourcePanel.parent instanceof LibraryVizPanel ? sourcePanel.parent.parent : sourcePanel.parent;
|
||||
const gridItem = sourcePanel.parent;
|
||||
|
||||
if (!(gridItem instanceof DashboardGridItem)) {
|
||||
console.error('VizPanel is not a child of a dashboard grid item');
|
||||
@ -143,8 +148,8 @@ export class VizPanelManager extends SceneObjectBase<VizPanelManagerState> {
|
||||
|
||||
private _detectPanelModelChanges = debounce(() => {
|
||||
const { hasChanges } = getPanelChanges(
|
||||
vizPanelToPanel(this.state.sourcePanel.resolve()),
|
||||
vizPanelToPanel(this.state.panel)
|
||||
vizPanelToPanel(this.state.sourcePanel.resolve().clone({ $behaviors: undefined })),
|
||||
vizPanelToPanel(this.state.panel.clone({ $behaviors: undefined }))
|
||||
);
|
||||
this.setState({ isDirty: hasChanges });
|
||||
}, 250);
|
||||
@ -263,10 +268,7 @@ export class VizPanelManager extends SceneObjectBase<VizPanelManagerState> {
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({
|
||||
pluginId,
|
||||
});
|
||||
|
||||
this.setState({ pluginId });
|
||||
this.state.panel.changePluginType(pluginId, cachedOptions, newFieldConfig);
|
||||
|
||||
this.loadDataSource();
|
||||
@ -405,21 +407,22 @@ export class VizPanelManager extends SceneObjectBase<VizPanelManagerState> {
|
||||
|
||||
public unlinkLibraryPanel() {
|
||||
const sourcePanel = this.state.sourcePanel.resolve();
|
||||
if (!(sourcePanel.parent instanceof LibraryVizPanel)) {
|
||||
throw new Error('VizPanel is not a child of a library panel');
|
||||
if (!isLibraryPanel(sourcePanel)) {
|
||||
throw new Error('VizPanel is not a library panel');
|
||||
}
|
||||
|
||||
const gridItem = sourcePanel.parent.parent;
|
||||
const gridItem = sourcePanel.parent;
|
||||
|
||||
if (!(gridItem instanceof DashboardGridItem)) {
|
||||
throw new Error('Library panel not a child of a grid item');
|
||||
}
|
||||
|
||||
const newSourcePanel = this.state.panel.clone({ $data: this.state.$data?.clone() });
|
||||
const newSourcePanel = this.state.panel.clone({ $data: sourcePanel.state.$data?.clone(), $behaviors: undefined });
|
||||
gridItem.setState({
|
||||
body: newSourcePanel,
|
||||
});
|
||||
|
||||
this.state.panel.setState({ $behaviors: undefined });
|
||||
this.setState({ sourcePanel: newSourcePanel.getRef() });
|
||||
}
|
||||
|
||||
@ -435,30 +438,17 @@ export class VizPanelManager extends SceneObjectBase<VizPanelManagerState> {
|
||||
maxPerRow: this.state.maxPerRow,
|
||||
};
|
||||
|
||||
const vizPanel = this.state.panel.clone();
|
||||
|
||||
if (sourcePanel.parent instanceof DashboardGridItem) {
|
||||
sourcePanel.parent.setState({
|
||||
...repeatUpdate,
|
||||
body: this.state.panel.clone(),
|
||||
body: vizPanel,
|
||||
});
|
||||
}
|
||||
|
||||
if (sourcePanel.parent instanceof LibraryVizPanel) {
|
||||
if (sourcePanel.parent.parent instanceof DashboardGridItem) {
|
||||
const newLibPanel = sourcePanel.parent.clone({
|
||||
panel: this.state.panel.clone(),
|
||||
});
|
||||
|
||||
sourcePanel.parent.parent.setState({
|
||||
body: newLibPanel,
|
||||
...repeatUpdate,
|
||||
});
|
||||
|
||||
updateLibraryVizPanel(newLibPanel!).then((p) => {
|
||||
if (sourcePanel.parent instanceof LibraryVizPanel) {
|
||||
newLibPanel.setPanelFromLibPanel(p);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (isLibraryPanel(vizPanel)) {
|
||||
saveLibPanel(vizPanel);
|
||||
}
|
||||
}
|
||||
|
||||
@ -467,9 +457,7 @@ export class VizPanelManager extends SceneObjectBase<VizPanelManagerState> {
|
||||
*/
|
||||
public getPanelSaveModel(): Panel | object {
|
||||
const sourcePanel = this.state.sourcePanel.resolve();
|
||||
|
||||
const isLibraryPanel = sourcePanel.parent instanceof LibraryVizPanel;
|
||||
const gridItem = isLibraryPanel ? sourcePanel.parent.parent : sourcePanel.parent;
|
||||
const gridItem = sourcePanel.parent;
|
||||
|
||||
if (!(gridItem instanceof DashboardGridItem)) {
|
||||
return { error: 'Unsupported panel parent' };
|
||||
|
@ -8,7 +8,9 @@ import { Dashboard } from '@grafana/schema/dist/esm/index.gen';
|
||||
import { ModalsContext, Modal, Button, useStyles2 } from '@grafana/ui';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
|
||||
import { SaveLibraryVizPanelModal } from '../panel-edit/SaveLibraryVizPanelModal';
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { getLibraryPanelBehavior, isLibraryPanel } from '../utils/utils';
|
||||
|
||||
interface DashboardPromptProps {
|
||||
dashboard: DashboardScene;
|
||||
@ -38,29 +40,39 @@ export const DashboardPrompt = memo(({ dashboard }: DashboardPromptProps) => {
|
||||
}, [dashboard]);
|
||||
|
||||
const onHistoryBlock = (location: H.Location) => {
|
||||
// const panelInEdit = dashboard.state.editPanel;
|
||||
// const search = new URLSearchParams(location.search);
|
||||
const panelInEdit = dashboard.state.editPanel;
|
||||
const vizPanelManager = panelInEdit?.state.vizManager;
|
||||
const vizPanel = vizPanelManager?.state.panel;
|
||||
const search = new URLSearchParams(location.search);
|
||||
|
||||
// TODO: Are we leaving panel edit & library panel?
|
||||
// Are we leaving panel edit & library panel?
|
||||
if (
|
||||
panelInEdit &&
|
||||
vizPanel &&
|
||||
isLibraryPanel(vizPanel) &&
|
||||
vizPanelManager.state.isDirty &&
|
||||
!search.has('editPanel')
|
||||
) {
|
||||
const libPanelBehavior = getLibraryPanelBehavior(vizPanel);
|
||||
|
||||
// if (panelInEdit && panelInEdit.libraryPanel && panelInEdit.hasChanged && !search.has('editPanel')) {
|
||||
// showModal(SaveLibraryPanelModal, {
|
||||
// isUnsavedPrompt: true,
|
||||
// panel: dashboard.panelInEdit as PanelModelWithLibraryPanel,
|
||||
// folderUid: dashboard.meta.folderUid ?? '',
|
||||
// onConfirm: () => {
|
||||
// hideModal();
|
||||
// moveToBlockedLocationAfterReactStateUpdate(location);
|
||||
// },
|
||||
// onDiscard: () => {
|
||||
// dispatch(discardPanelChanges());
|
||||
// moveToBlockedLocationAfterReactStateUpdate(location);
|
||||
// hideModal();
|
||||
// },
|
||||
// onDismiss: hideModal,
|
||||
// });
|
||||
// return false;
|
||||
// }
|
||||
showModal(SaveLibraryVizPanelModal, {
|
||||
dashboard,
|
||||
isUnsavedPrompt: true,
|
||||
libraryPanel: libPanelBehavior!,
|
||||
onConfirm: () => {
|
||||
panelInEdit.onConfirmSaveLibraryPanel();
|
||||
hideModal();
|
||||
moveToBlockedLocationAfterReactStateUpdate(location);
|
||||
},
|
||||
onDiscard: () => {
|
||||
panelInEdit.onDiscard();
|
||||
hideModal();
|
||||
moveToBlockedLocationAfterReactStateUpdate(location);
|
||||
},
|
||||
onDismiss: hideModal,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
// Are we still on the same dashboard?
|
||||
if (originalPath === location.pathname) {
|
||||
|
@ -19,7 +19,7 @@ import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsData
|
||||
import { DashboardControls } from '../scene/DashboardControls';
|
||||
import { DashboardGridItem } from '../scene/DashboardGridItem';
|
||||
import { DashboardScene, PERSISTED_PROPS } from '../scene/DashboardScene';
|
||||
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
|
||||
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
|
||||
import { VizPanelLinks } from '../scene/PanelLinks';
|
||||
import { PanelTimeRange } from '../scene/PanelTimeRange';
|
||||
import { transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel';
|
||||
@ -77,11 +77,6 @@ export class DashboardSceneChangeTracker {
|
||||
if (payload.changedObject instanceof VizPanelLinks) {
|
||||
return true;
|
||||
}
|
||||
if (payload.changedObject instanceof LibraryVizPanel) {
|
||||
if (Object.prototype.hasOwnProperty.call(payload.partialUpdate, 'name')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (payload.changedObject instanceof SceneRefreshPicker) {
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(payload.partialUpdate, 'intervals') ||
|
||||
@ -90,6 +85,11 @@ export class DashboardSceneChangeTracker {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (payload.changedObject instanceof LibraryPanelBehavior) {
|
||||
if (Object.prototype.hasOwnProperty.call(payload.partialUpdate, 'name')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (payload.changedObject instanceof behaviors.CursorSync) {
|
||||
return true;
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { SceneGridLayout, SceneGridRow, SceneTimeRange } from '@grafana/scenes';
|
||||
import { SceneGridLayout, SceneGridRow, SceneTimeRange, VizPanel } from '@grafana/scenes';
|
||||
import { LibraryPanel } from '@grafana/schema/dist/esm/index.gen';
|
||||
|
||||
import { activateFullSceneTree } from '../utils/test-utils';
|
||||
@ -6,7 +6,17 @@ import { activateFullSceneTree } from '../utils/test-utils';
|
||||
import { AddLibraryPanelDrawer } from './AddLibraryPanelDrawer';
|
||||
import { DashboardGridItem } from './DashboardGridItem';
|
||||
import { DashboardScene } from './DashboardScene';
|
||||
import { LibraryVizPanel } from './LibraryVizPanel';
|
||||
import { LibraryPanelBehavior } from './LibraryPanelBehavior';
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
getDataSourceSrv: () => {
|
||||
return {
|
||||
get: jest.fn().mockResolvedValue({}),
|
||||
getInstanceSettings: jest.fn().mockResolvedValue({ uid: 'ds1' }),
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
describe('AddLibraryPanelWidget', () => {
|
||||
let dashboard: DashboardScene;
|
||||
@ -35,8 +45,55 @@ describe('AddLibraryPanelWidget', () => {
|
||||
const gridItem = layout.state.children[0] as DashboardGridItem;
|
||||
|
||||
expect(layout.state.children.length).toBe(1);
|
||||
expect(gridItem.state.body!).toBeInstanceOf(LibraryVizPanel);
|
||||
expect((gridItem.state.body! as LibraryVizPanel).state.panelKey).toBe('panel-1');
|
||||
expect(gridItem.state.body!).toBeInstanceOf(VizPanel);
|
||||
expect(gridItem.state.body.state.$behaviors![0]).toBeInstanceOf(LibraryPanelBehavior);
|
||||
expect(gridItem.state.body.state.key).toBe('panel-1');
|
||||
});
|
||||
|
||||
it('should add library panel from menu and enter edit mode in a dashboard that is not already in edit mode', async () => {
|
||||
const drawer = new AddLibraryPanelDrawer({});
|
||||
const dashboard = new DashboardScene({
|
||||
$timeRange: new SceneTimeRange({}),
|
||||
title: 'hello',
|
||||
uid: 'dash-1',
|
||||
version: 4,
|
||||
meta: {
|
||||
canEdit: true,
|
||||
},
|
||||
body: new SceneGridLayout({
|
||||
children: [],
|
||||
}),
|
||||
overlay: drawer,
|
||||
});
|
||||
|
||||
activateFullSceneTree(dashboard);
|
||||
|
||||
await new Promise((r) => setTimeout(r, 1));
|
||||
|
||||
const panelInfo: LibraryPanel = {
|
||||
uid: 'uid',
|
||||
model: {
|
||||
type: 'timeseries',
|
||||
},
|
||||
name: 'name',
|
||||
version: 1,
|
||||
type: 'timeseries',
|
||||
};
|
||||
|
||||
// if we are in a saved dashboard with no panels, adding a lib panel through
|
||||
// the CTA should enter edit mode
|
||||
expect(dashboard.state.isEditing).toBe(undefined);
|
||||
|
||||
drawer.onAddLibraryPanel(panelInfo);
|
||||
|
||||
const layout = dashboard.state.body as SceneGridLayout;
|
||||
const gridItem = layout.state.children[0] as DashboardGridItem;
|
||||
|
||||
expect(layout.state.children.length).toBe(1);
|
||||
expect(gridItem.state.body!).toBeInstanceOf(VizPanel);
|
||||
expect(gridItem.state.body.state.$behaviors![0]).toBeInstanceOf(LibraryPanelBehavior);
|
||||
expect(gridItem.state.body.state.key).toBe('panel-1');
|
||||
expect(dashboard.state.isEditing).toBe(true);
|
||||
});
|
||||
|
||||
it('should throw error if adding lib panel in a layout that is not SceneGridLayout', () => {
|
||||
@ -48,11 +105,11 @@ describe('AddLibraryPanelWidget', () => {
|
||||
});
|
||||
|
||||
it('should replace grid item when grid item state is passed', async () => {
|
||||
const libPanel = new LibraryVizPanel({
|
||||
const libPanel = new VizPanel({
|
||||
title: 'Panel Title',
|
||||
uid: 'uid',
|
||||
name: 'name',
|
||||
panelKey: 'panel-1',
|
||||
pluginId: 'table',
|
||||
key: 'panel-1',
|
||||
$behaviors: [new LibraryPanelBehavior({ title: 'LibraryPanel A title', name: 'LibraryPanel A', uid: 'uid' })],
|
||||
});
|
||||
|
||||
let gridItem = new DashboardGridItem({
|
||||
@ -88,20 +145,22 @@ describe('AddLibraryPanelWidget', () => {
|
||||
|
||||
const layout = dashboard.state.body as SceneGridLayout;
|
||||
gridItem = layout.state.children[0] as DashboardGridItem;
|
||||
const behavior = gridItem.state.body!.state.$behaviors![0] as LibraryPanelBehavior;
|
||||
|
||||
expect(layout.state.children.length).toBe(1);
|
||||
expect(gridItem.state.body!).toBeInstanceOf(LibraryVizPanel);
|
||||
expect(gridItem.state.body!).toBeInstanceOf(VizPanel);
|
||||
expect(behavior).toBeInstanceOf(LibraryPanelBehavior);
|
||||
expect(gridItem.state.key).toBe('grid-item-1');
|
||||
expect((gridItem.state.body! as LibraryVizPanel).state.uid).toBe('new_uid');
|
||||
expect((gridItem.state.body! as LibraryVizPanel).state.name).toBe('new_name');
|
||||
expect(behavior.state.uid).toBe('new_uid');
|
||||
expect(behavior.state.name).toBe('new_name');
|
||||
});
|
||||
|
||||
it('should replace grid item in row when grid item state is passed', async () => {
|
||||
const libPanel = new LibraryVizPanel({
|
||||
const libPanel = new VizPanel({
|
||||
title: 'Panel Title',
|
||||
uid: 'uid',
|
||||
name: 'name',
|
||||
panelKey: 'panel-1',
|
||||
pluginId: 'table',
|
||||
key: 'panel-1',
|
||||
$behaviors: [new LibraryPanelBehavior({ title: 'LibraryPanel A title', name: 'LibraryPanel A', uid: 'uid' })],
|
||||
});
|
||||
|
||||
let gridItem = new DashboardGridItem({
|
||||
@ -142,13 +201,15 @@ describe('AddLibraryPanelWidget', () => {
|
||||
const layout = dashboard.state.body as SceneGridLayout;
|
||||
const gridRow = layout.state.children[0] as SceneGridRow;
|
||||
gridItem = gridRow.state.children[0] as DashboardGridItem;
|
||||
const behavior = gridItem.state.body!.state.$behaviors![0] as LibraryPanelBehavior;
|
||||
|
||||
expect(layout.state.children.length).toBe(1);
|
||||
expect(gridRow.state.children.length).toBe(1);
|
||||
expect(gridItem.state.body!).toBeInstanceOf(LibraryVizPanel);
|
||||
expect(gridItem.state.body!).toBeInstanceOf(VizPanel);
|
||||
expect(behavior).toBeInstanceOf(LibraryPanelBehavior);
|
||||
expect(gridItem.state.key).toBe('grid-item-1');
|
||||
expect((gridItem.state.body! as LibraryVizPanel).state.uid).toBe('new_uid');
|
||||
expect((gridItem.state.body! as LibraryVizPanel).state.name).toBe('new_name');
|
||||
expect(behavior.state.uid).toBe('new_uid');
|
||||
expect(behavior.state.name).toBe('new_name');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -4,6 +4,7 @@ import {
|
||||
SceneObjectBase,
|
||||
SceneObjectRef,
|
||||
SceneObjectState,
|
||||
VizPanel,
|
||||
} from '@grafana/scenes';
|
||||
import { LibraryPanel } from '@grafana/schema';
|
||||
import { Drawer } from '@grafana/ui';
|
||||
@ -14,13 +15,13 @@ import {
|
||||
} from 'app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch';
|
||||
|
||||
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
|
||||
import { NEW_PANEL_HEIGHT, NEW_PANEL_WIDTH, getDashboardSceneFor, getVizPanelKeyForPanelId } from '../utils/utils';
|
||||
import { NEW_PANEL_HEIGHT, NEW_PANEL_WIDTH, getDashboardSceneFor, getDefaultVizPanel } from '../utils/utils';
|
||||
|
||||
import { DashboardGridItem } from './DashboardGridItem';
|
||||
import { LibraryVizPanel } from './LibraryVizPanel';
|
||||
import { LibraryPanelBehavior } from './LibraryPanelBehavior';
|
||||
|
||||
export interface AddLibraryPanelDrawerState extends SceneObjectState {
|
||||
panelToReplaceRef?: SceneObjectRef<LibraryVizPanel>;
|
||||
panelToReplaceRef?: SceneObjectRef<VizPanel>;
|
||||
}
|
||||
|
||||
export class AddLibraryPanelDrawer extends SceneObjectBase<AddLibraryPanelDrawerState> {
|
||||
@ -38,11 +39,9 @@ export class AddLibraryPanelDrawer extends SceneObjectBase<AddLibraryPanelDrawer
|
||||
|
||||
const panelId = dashboardSceneGraph.getNextPanelId(dashboard);
|
||||
|
||||
const body = new LibraryVizPanel({
|
||||
title: 'Panel Title',
|
||||
uid: panelInfo.uid,
|
||||
name: panelInfo.name,
|
||||
panelKey: getVizPanelKeyForPanelId(panelId),
|
||||
const body = getDefaultVizPanel(dashboard);
|
||||
body.setState({
|
||||
$behaviors: [new LibraryPanelBehavior({ uid: panelInfo.uid, name: panelInfo.name })],
|
||||
});
|
||||
|
||||
const panelToReplace = this.state.panelToReplaceRef?.resolve();
|
||||
@ -66,6 +65,10 @@ export class AddLibraryPanelDrawer extends SceneObjectBase<AddLibraryPanelDrawer
|
||||
});
|
||||
|
||||
layout.setState({ children: [newGridItem, ...layout.state.children] });
|
||||
|
||||
if (!dashboard.state.isEditing) {
|
||||
dashboard.onEnterEditMode();
|
||||
}
|
||||
}
|
||||
|
||||
this.onClose();
|
||||
|
@ -21,7 +21,7 @@ import { activateFullSceneTree } from '../utils/test-utils';
|
||||
import { DashboardDatasourceBehaviour } from './DashboardDatasourceBehaviour';
|
||||
import { DashboardGridItem } from './DashboardGridItem';
|
||||
import { DashboardScene } from './DashboardScene';
|
||||
import { LibraryVizPanel } from './LibraryVizPanel';
|
||||
import { LibraryPanelBehavior } from './LibraryPanelBehavior';
|
||||
|
||||
const grafanaDs = {
|
||||
id: 1,
|
||||
@ -491,16 +491,23 @@ describe('DashboardDatasourceBehaviour', () => {
|
||||
});
|
||||
|
||||
describe('Library panels', () => {
|
||||
it('should wait for library panel to be loaded', async () => {
|
||||
const sourcePanel = new LibraryVizPanel({
|
||||
name: 'My Library Panel',
|
||||
it('should re-run queries when library panel re-runs query', async () => {
|
||||
const libPanelBehavior = new LibraryPanelBehavior({
|
||||
isLoaded: false,
|
||||
title: 'Panel title',
|
||||
uid: 'fdcvggvfy2qdca',
|
||||
panelKey: 'lib-panel',
|
||||
panel: new VizPanel({
|
||||
key: 'panel-1',
|
||||
title: 'Panel A',
|
||||
pluginId: 'table',
|
||||
name: 'My Library Panel',
|
||||
_loadedPanel: undefined,
|
||||
});
|
||||
|
||||
const sourcePanel = new VizPanel({
|
||||
key: 'panel-1',
|
||||
title: 'Panel A',
|
||||
pluginId: 'table',
|
||||
$behaviors: [libPanelBehavior],
|
||||
$data: new SceneQueryRunner({
|
||||
datasource: { uid: 'grafana' },
|
||||
queries: [{ refId: 'A', queryType: 'randomWalk' }],
|
||||
}),
|
||||
});
|
||||
|
||||
@ -544,29 +551,22 @@ describe('DashboardDatasourceBehaviour', () => {
|
||||
}),
|
||||
});
|
||||
|
||||
activateFullSceneTree(scene);
|
||||
const sceneDeactivate = activateFullSceneTree(scene);
|
||||
|
||||
// spy on runQueries
|
||||
const spy = jest.spyOn(dashboardDSPanel.state.$data as SceneQueryRunner, 'runQueries');
|
||||
|
||||
// deactivate scene to mimic going into panel edit
|
||||
sceneDeactivate();
|
||||
|
||||
// run source panel queries and update request ID
|
||||
(sourcePanel.state.$data as SceneQueryRunner).runQueries();
|
||||
|
||||
// // activate scene to mimic coming back from panel edit
|
||||
activateFullSceneTree(scene);
|
||||
|
||||
await new Promise((r) => setTimeout(r, 1));
|
||||
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
|
||||
// Simulate library panel being loaded
|
||||
sourcePanel.setState({
|
||||
isLoaded: true,
|
||||
panel: new VizPanel({
|
||||
title: 'Panel A',
|
||||
pluginId: 'table',
|
||||
key: 'panel-1',
|
||||
$data: new SceneQueryRunner({
|
||||
datasource: { uid: 'grafana' },
|
||||
queries: [{ refId: 'A', queryType: 'randomWalk' }],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
@ -1,18 +1,9 @@
|
||||
import { Unsubscribable } from 'rxjs';
|
||||
|
||||
import {
|
||||
CancelActivationHandler,
|
||||
SceneObjectBase,
|
||||
SceneObjectState,
|
||||
SceneQueryRunner,
|
||||
VizPanel,
|
||||
} from '@grafana/scenes';
|
||||
import { SceneObjectBase, SceneObjectState, SceneQueryRunner, VizPanel } from '@grafana/scenes';
|
||||
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard';
|
||||
|
||||
import { findVizPanelByKey, getDashboardSceneFor, getQueryRunnerFor, getVizPanelKeyForPanelId } from '../utils/utils';
|
||||
|
||||
import { DashboardScene } from './DashboardScene';
|
||||
import { LibraryVizPanel, LibraryVizPanelState } from './LibraryVizPanel';
|
||||
|
||||
interface DashboardDatasourceBehaviourState extends SceneObjectState {}
|
||||
|
||||
@ -27,8 +18,6 @@ export class DashboardDatasourceBehaviour extends SceneObjectBase<DashboardDatas
|
||||
private _activationHandler() {
|
||||
const queryRunner = this.parent;
|
||||
let dashboard: DashboardScene;
|
||||
let libraryPanelSub: Unsubscribable;
|
||||
|
||||
if (!(queryRunner instanceof SceneQueryRunner)) {
|
||||
throw new Error('DashboardDatasourceBehaviour must be attached to a SceneQueryRunner');
|
||||
}
|
||||
@ -59,47 +48,16 @@ export class DashboardDatasourceBehaviour extends SceneObjectBase<DashboardDatas
|
||||
|
||||
const sourcePanelQueryRunner = getQueryRunnerFor(panel);
|
||||
|
||||
let parentLibPanelCleanUp: undefined | CancelActivationHandler;
|
||||
|
||||
if (!sourcePanelQueryRunner) {
|
||||
if (!(panel.parent instanceof LibraryVizPanel)) {
|
||||
throw new Error('Could not find SceneQueryRunner for panel');
|
||||
} else {
|
||||
if (!panel.parent.isActive) {
|
||||
parentLibPanelCleanUp = panel.parent.activate();
|
||||
}
|
||||
// Library panels load and create internal viz panel asynchroniously. Here we are subscribing to
|
||||
// library panel state, and run dashboard queries when the source panel query runner is ready.
|
||||
libraryPanelSub = panel.parent.subscribeToState((n, p) => {
|
||||
this.handleLibPanelStateUpdates(n, p, queryRunner);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (this.prevRequestId && this.prevRequestId !== sourcePanelQueryRunner.state.data?.request?.requestId) {
|
||||
queryRunner.runQueries();
|
||||
}
|
||||
throw new Error('Could not find SceneQueryRunner for panel');
|
||||
}
|
||||
|
||||
if (this.prevRequestId && this.prevRequestId !== sourcePanelQueryRunner.state.data?.request?.requestId) {
|
||||
queryRunner.runQueries();
|
||||
}
|
||||
|
||||
return () => {
|
||||
this.prevRequestId = sourcePanelQueryRunner?.state.data?.request?.requestId;
|
||||
if (libraryPanelSub) {
|
||||
libraryPanelSub.unsubscribe();
|
||||
}
|
||||
if (parentLibPanelCleanUp) {
|
||||
parentLibPanelCleanUp();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private handleLibPanelStateUpdates(n: LibraryVizPanelState, p: LibraryVizPanelState, queryRunner: SceneQueryRunner) {
|
||||
if (n.panel && n.panel !== p.panel) {
|
||||
const libPanelQueryRunner = getQueryRunnerFor(n.panel);
|
||||
|
||||
if (!(libPanelQueryRunner instanceof SceneQueryRunner)) {
|
||||
throw new Error('Could not find SceneQueryRunner for panel');
|
||||
}
|
||||
|
||||
queryRunner.runQueries();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { isEqual } from 'lodash';
|
||||
import { useMemo } from 'react';
|
||||
import { Unsubscribable } from 'rxjs';
|
||||
|
||||
import { config } from '@grafana/runtime';
|
||||
import {
|
||||
@ -26,13 +25,11 @@ import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants';
|
||||
|
||||
import { getMultiVariableValues, getQueryRunnerFor } from '../utils/utils';
|
||||
|
||||
import { AddLibraryPanelDrawer } from './AddLibraryPanelDrawer';
|
||||
import { LibraryVizPanel } from './LibraryVizPanel';
|
||||
import { repeatPanelMenuBehavior } from './PanelMenuBehavior';
|
||||
import { DashboardRepeatsProcessedEvent } from './types';
|
||||
|
||||
export interface DashboardGridItemState extends SceneGridItemStateLike {
|
||||
body: VizPanel | LibraryVizPanel | AddLibraryPanelDrawer;
|
||||
body: VizPanel;
|
||||
repeatedPanels?: VizPanel[];
|
||||
variableName?: string;
|
||||
itemHeight?: number;
|
||||
@ -43,9 +40,8 @@ export interface DashboardGridItemState extends SceneGridItemStateLike {
|
||||
export type RepeatDirection = 'v' | 'h';
|
||||
|
||||
export class DashboardGridItem extends SceneObjectBase<DashboardGridItemState> implements SceneGridItemLike {
|
||||
private _libPanelSubscription: Unsubscribable | undefined;
|
||||
private _prevRepeatValues?: VariableValueSingle[];
|
||||
private _oldBody?: VizPanel | LibraryVizPanel | AddLibraryPanelDrawer;
|
||||
private _oldBody?: VizPanel;
|
||||
|
||||
protected _variableDependency = new DashboardGridItemVariableDependencyHandler(this);
|
||||
|
||||
@ -65,45 +61,6 @@ export class DashboardGridItem extends SceneObjectBase<DashboardGridItemState> i
|
||||
this._oldBody = this.state.body;
|
||||
this.performRepeat();
|
||||
}
|
||||
|
||||
// Subscriptions that handles body updates, i.e. VizPanel -> LibraryVizPanel, AddLibPanelWidget -> LibraryVizPanel
|
||||
this._subs.add(
|
||||
this.subscribeToState((newState, prevState) => {
|
||||
if (newState.body !== prevState.body) {
|
||||
if (newState.body instanceof LibraryVizPanel) {
|
||||
this.setupLibraryPanelChangeSubscription(newState.body);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Initial setup of the lbrary panel subscription. Lib panels are lazy laded, so only then we can subscribe to the repeat config changes
|
||||
if (this.state.body instanceof LibraryVizPanel) {
|
||||
this.setupLibraryPanelChangeSubscription(this.state.body);
|
||||
}
|
||||
|
||||
return () => {
|
||||
this._libPanelSubscription?.unsubscribe();
|
||||
this._libPanelSubscription = undefined;
|
||||
};
|
||||
}
|
||||
|
||||
private setupLibraryPanelChangeSubscription(panel: LibraryVizPanel) {
|
||||
if (this._libPanelSubscription) {
|
||||
this._libPanelSubscription.unsubscribe();
|
||||
this._libPanelSubscription = undefined;
|
||||
}
|
||||
|
||||
this._libPanelSubscription = panel.subscribeToState((newState) => {
|
||||
if (newState._loadedPanel?.model.repeat) {
|
||||
this.setState({
|
||||
variableName: newState._loadedPanel.model.repeat,
|
||||
repeatDirection: newState._loadedPanel.model.repeatDirection,
|
||||
maxPerRow: newState._loadedPanel.model.maxPerRow,
|
||||
});
|
||||
this.performRepeat();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -132,10 +89,6 @@ export class DashboardGridItem extends SceneObjectBase<DashboardGridItemState> i
|
||||
}
|
||||
|
||||
public performRepeat() {
|
||||
if (this.state.body instanceof AddLibraryPanelDrawer) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.state.variableName || sceneGraph.hasVariableDependencyInLoadingState(this)) {
|
||||
return;
|
||||
}
|
||||
@ -165,7 +118,7 @@ export class DashboardGridItem extends SceneObjectBase<DashboardGridItemState> i
|
||||
}
|
||||
|
||||
this._prevRepeatValues = values;
|
||||
const panelToRepeat = this.state.body instanceof LibraryVizPanel ? this.state.body.state.panel! : this.state.body;
|
||||
const panelToRepeat = this.state.body;
|
||||
const repeatedPanels: VizPanel[] = [];
|
||||
|
||||
// when variable has no options (due to error or similar) it will not render any panels at all
|
||||
@ -262,14 +215,6 @@ export class DashboardGridItem extends SceneObjectBase<DashboardGridItemState> i
|
||||
if (body instanceof VizPanel) {
|
||||
return <body.Component model={body} key={body.state.key} />;
|
||||
}
|
||||
|
||||
if (body instanceof LibraryVizPanel) {
|
||||
return <body.Component model={body} key={body.state.key} />;
|
||||
}
|
||||
|
||||
if (body instanceof AddLibraryPanelDrawer) {
|
||||
return <body.Component model={body} key={body.state.key} />;
|
||||
}
|
||||
}
|
||||
|
||||
if (!repeatedPanels) {
|
||||
|
@ -20,11 +20,7 @@ import { VariablesChanged } from 'app/features/variables/types';
|
||||
|
||||
import { PanelEditor, buildPanelEditScene } from '../panel-edit/PanelEditor';
|
||||
import { createWorker } from '../saving/createDetectChangesWorker';
|
||||
import {
|
||||
buildGridItemForLibPanel,
|
||||
buildGridItemForPanel,
|
||||
transformSaveModelToScene,
|
||||
} from '../serialization/transformSaveModelToScene';
|
||||
import { buildGridItemForPanel, transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
|
||||
import { DecoratedRevisionModel } from '../settings/VersionsEditView';
|
||||
import { historySrv } from '../settings/version-history/HistorySrv';
|
||||
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
|
||||
@ -34,7 +30,7 @@ import { findVizPanelByKey } from '../utils/utils';
|
||||
import { DashboardControls } from './DashboardControls';
|
||||
import { DashboardGridItem } from './DashboardGridItem';
|
||||
import { DashboardScene, DashboardSceneState } from './DashboardScene';
|
||||
import { LibraryVizPanel } from './LibraryVizPanel';
|
||||
import { LibraryPanelBehavior } from './LibraryPanelBehavior';
|
||||
import { PanelTimeRange } from './PanelTimeRange';
|
||||
import { RowActions } from './row-actions/RowActions';
|
||||
|
||||
@ -348,20 +344,21 @@ describe('DashboardScene', () => {
|
||||
expect(restoredDashboardGridItem.state.variableName).toBe(prevValue);
|
||||
});
|
||||
|
||||
it('A change to any LibraryVizPanel name should set isDirty true', () => {
|
||||
const libraryVizPanel = sceneGraph.findObject(scene, (p) => p instanceof LibraryVizPanel) as LibraryVizPanel;
|
||||
const prevValue = libraryVizPanel.state.name;
|
||||
it('A change to any library panel name should set isDirty true', () => {
|
||||
const libraryVizPanel = ((scene.state.body as SceneGridLayout).state.children[4] as DashboardGridItem).state
|
||||
.body;
|
||||
const behavior = libraryVizPanel.state.$behaviors![0] as LibraryPanelBehavior;
|
||||
const prevValue = behavior.state.name;
|
||||
|
||||
libraryVizPanel.setState({ name: 'new name' });
|
||||
behavior.setState({ name: 'new name' });
|
||||
|
||||
expect(scene.state.isDirty).toBe(true);
|
||||
|
||||
scene.exitEditMode({ skipConfirm: true });
|
||||
const restoredLibraryVizPanel = sceneGraph.findObject(
|
||||
scene,
|
||||
(p) => p instanceof LibraryVizPanel
|
||||
) as LibraryVizPanel;
|
||||
expect(restoredLibraryVizPanel.state.name).toBe(prevValue);
|
||||
const restoredLibraryVizPanel = ((scene.state.body as SceneGridLayout).state.children[4] as DashboardGridItem)
|
||||
.state.body;
|
||||
const restoredBehavior = restoredLibraryVizPanel.state.$behaviors![0] as LibraryPanelBehavior;
|
||||
expect(restoredBehavior.state.name).toBe(prevValue);
|
||||
});
|
||||
|
||||
it('A change to any PanelTimeRange state should set isDirty true', () => {
|
||||
@ -625,19 +622,14 @@ describe('DashboardScene', () => {
|
||||
});
|
||||
|
||||
it('Should fail to copy a library panel if it does not have a grid item parent', () => {
|
||||
const libVizPanel = new LibraryVizPanel({
|
||||
uid: 'uid',
|
||||
name: 'libraryPanel',
|
||||
panelKey: 'panel-4',
|
||||
const libVizPanel = new VizPanel({
|
||||
title: 'Library Panel',
|
||||
panel: new VizPanel({
|
||||
title: 'Library Panel',
|
||||
key: 'panel-4',
|
||||
pluginId: 'table',
|
||||
}),
|
||||
pluginId: 'table',
|
||||
key: 'panel-4',
|
||||
$behaviors: [new LibraryPanelBehavior({ title: 'Library Panel', name: 'libraryPanel', uid: 'uid' })],
|
||||
});
|
||||
|
||||
scene.copyPanel(libVizPanel.state.panel as VizPanel);
|
||||
scene.copyPanel(libVizPanel);
|
||||
|
||||
expect(store.exists(LS_PANEL_COPY_KEY)).toBe(false);
|
||||
});
|
||||
@ -650,10 +642,11 @@ describe('DashboardScene', () => {
|
||||
});
|
||||
|
||||
it('Should copy a library viz panel', () => {
|
||||
const libVizPanel = ((scene.state.body as SceneGridLayout).state.children[4] as DashboardGridItem).state
|
||||
.body as LibraryVizPanel;
|
||||
const libVizPanel = ((scene.state.body as SceneGridLayout).state.children[4] as DashboardGridItem).state.body;
|
||||
|
||||
scene.copyPanel(libVizPanel.state.panel as VizPanel);
|
||||
expect(libVizPanel.state.$behaviors![0]).toBeInstanceOf(LibraryPanelBehavior);
|
||||
|
||||
scene.copyPanel(libVizPanel);
|
||||
|
||||
expect(store.exists(LS_PANEL_COPY_KEY)).toBe(true);
|
||||
});
|
||||
@ -687,13 +680,13 @@ describe('DashboardScene', () => {
|
||||
it('Should paste a library viz panel', () => {
|
||||
store.set(LS_PANEL_COPY_KEY, JSON.stringify({ key: 'panel-7' }));
|
||||
jest.spyOn(JSON, 'parse').mockReturnValue({ libraryPanel: { uid: 'uid', name: 'libraryPanel' } });
|
||||
jest.mocked(buildGridItemForLibPanel).mockReturnValue(
|
||||
jest.mocked(buildGridItemForPanel).mockReturnValue(
|
||||
new DashboardGridItem({
|
||||
body: new LibraryVizPanel({
|
||||
body: new VizPanel({
|
||||
title: 'Library Panel',
|
||||
uid: 'uid',
|
||||
name: 'libraryPanel',
|
||||
panelKey: 'panel-4',
|
||||
pluginId: 'table',
|
||||
key: 'panel-4',
|
||||
$behaviors: [new LibraryPanelBehavior({ title: 'Library Panel', name: 'libraryPanel', uid: 'uid' })],
|
||||
}),
|
||||
})
|
||||
);
|
||||
@ -703,12 +696,11 @@ describe('DashboardScene', () => {
|
||||
const body = scene.state.body as SceneGridLayout;
|
||||
const gridItem = body.state.children[0] as DashboardGridItem;
|
||||
|
||||
const libVizPanel = gridItem.state.body as LibraryVizPanel;
|
||||
const libVizPanel = gridItem.state.body;
|
||||
|
||||
expect(buildGridItemForLibPanel).toHaveBeenCalledTimes(1);
|
||||
expect(buildGridItemForPanel).toHaveBeenCalledTimes(1);
|
||||
expect(body.state.children.length).toBe(6);
|
||||
expect(libVizPanel.state.panelKey).toBe('panel-7');
|
||||
expect(libVizPanel.state.panel?.state.key).toBe('panel-7');
|
||||
expect(libVizPanel.state.key).toBe('panel-7');
|
||||
expect(gridItem.state.y).toBe(0);
|
||||
expect(store.exists(LS_PANEL_COPY_KEY)).toBe(false);
|
||||
});
|
||||
@ -736,8 +728,7 @@ describe('DashboardScene', () => {
|
||||
|
||||
it('Should remove a library panel', () => {
|
||||
const libraryPanel = ((scene.state.body as SceneGridLayout).state.children[4] as DashboardGridItem).state.body;
|
||||
const vizPanel = (libraryPanel as LibraryVizPanel).state.panel;
|
||||
scene.removePanel(vizPanel as VizPanel);
|
||||
scene.removePanel(libraryPanel);
|
||||
|
||||
const body = scene.state.body as SceneGridLayout;
|
||||
expect(body.state.children.length).toBe(4);
|
||||
@ -748,9 +739,8 @@ describe('DashboardScene', () => {
|
||||
((scene.state.body as SceneGridLayout).state.children[2] as SceneGridRow).state
|
||||
.children[1] as DashboardGridItem
|
||||
).state.body;
|
||||
const vizPanel = (libraryPanel as LibraryVizPanel).state.panel;
|
||||
|
||||
scene.removePanel(vizPanel as VizPanel);
|
||||
scene.removePanel(libraryPanel);
|
||||
|
||||
const body = scene.state.body as SceneGridLayout;
|
||||
const gridRow = body.state.children[2] as SceneGridRow;
|
||||
@ -784,17 +774,15 @@ describe('DashboardScene', () => {
|
||||
|
||||
it('Should duplicate a library panel', () => {
|
||||
const libraryPanel = ((scene.state.body as SceneGridLayout).state.children[4] as DashboardGridItem).state.body;
|
||||
const vizPanel = (libraryPanel as LibraryVizPanel).state.panel;
|
||||
scene.duplicatePanel(vizPanel as VizPanel);
|
||||
scene.duplicatePanel(libraryPanel);
|
||||
|
||||
const body = scene.state.body as SceneGridLayout;
|
||||
const gridItem = body.state.children[5] as DashboardGridItem;
|
||||
|
||||
const libVizPanel = gridItem.state.body as LibraryVizPanel;
|
||||
const libVizPanel = gridItem.state.body;
|
||||
|
||||
expect(body.state.children.length).toBe(6);
|
||||
expect(libVizPanel.state.panelKey).toBe('panel-7');
|
||||
expect(libVizPanel.state.panel?.state.key).toBe('panel-7');
|
||||
expect(libVizPanel.state.key).toBe('panel-7');
|
||||
});
|
||||
|
||||
it('Should duplicate a repeated panel', () => {
|
||||
@ -855,19 +843,17 @@ describe('DashboardScene', () => {
|
||||
((scene.state.body as SceneGridLayout).state.children[2] as SceneGridRow).state
|
||||
.children[1] as DashboardGridItem
|
||||
).state.body;
|
||||
const vizPanel = (libraryPanel as LibraryVizPanel).state.panel;
|
||||
|
||||
scene.duplicatePanel(vizPanel as VizPanel);
|
||||
scene.duplicatePanel(libraryPanel);
|
||||
|
||||
const body = scene.state.body as SceneGridLayout;
|
||||
const gridRow = body.state.children[2] as SceneGridRow;
|
||||
const gridItem = gridRow.state.children[2] as DashboardGridItem;
|
||||
|
||||
const libVizPanel = gridItem.state.body as LibraryVizPanel;
|
||||
const libVizPanel = gridItem.state.body;
|
||||
|
||||
expect(gridRow.state.children.length).toBe(3);
|
||||
expect(libVizPanel.state.panelKey).toBe('panel-7');
|
||||
expect(libVizPanel.state.panel?.state.key).toBe('panel-7');
|
||||
expect(libVizPanel.state.key).toBe('panel-7');
|
||||
});
|
||||
|
||||
it('Should fail to duplicate a panel if it does not have a grid item parent', () => {
|
||||
@ -886,16 +872,10 @@ describe('DashboardScene', () => {
|
||||
});
|
||||
|
||||
it('Should unlink a library panel', () => {
|
||||
const libPanel = new LibraryVizPanel({
|
||||
title: 'title',
|
||||
uid: 'abc',
|
||||
name: 'lib panel',
|
||||
panelKey: 'panel-1',
|
||||
isLoaded: true,
|
||||
panel: new VizPanel({
|
||||
title: 'Panel B',
|
||||
pluginId: 'table',
|
||||
}),
|
||||
const libPanel = new VizPanel({
|
||||
title: 'Panel B',
|
||||
pluginId: 'table',
|
||||
$behaviors: [new LibraryPanelBehavior({ title: 'title', name: 'lib panel', uid: 'abc', isLoaded: true })],
|
||||
});
|
||||
|
||||
const scene = buildTestScene({
|
||||
@ -916,6 +896,7 @@ describe('DashboardScene', () => {
|
||||
|
||||
expect(body.state.children.length).toBe(1);
|
||||
expect(gridItem.state.body).toBeInstanceOf(VizPanel);
|
||||
expect(gridItem.state.$behaviors).toBeUndefined();
|
||||
});
|
||||
|
||||
it('Should create a library panel', () => {
|
||||
@ -945,11 +926,12 @@ describe('DashboardScene', () => {
|
||||
|
||||
const layout = scene.state.body as SceneGridLayout;
|
||||
const newGridItem = layout.state.children[0] as DashboardGridItem;
|
||||
const behavior = newGridItem.state.body.state.$behaviors![0] as LibraryPanelBehavior;
|
||||
|
||||
expect(layout.state.children.length).toBe(1);
|
||||
expect(newGridItem.state.body).toBeInstanceOf(LibraryVizPanel);
|
||||
expect((newGridItem.state.body as LibraryVizPanel).state.uid).toBe('uid');
|
||||
expect((newGridItem.state.body as LibraryVizPanel).state.name).toBe('name');
|
||||
expect(newGridItem.state.body).toBeInstanceOf(VizPanel);
|
||||
expect(behavior.state.uid).toBe('uid');
|
||||
expect(behavior.state.name).toBe('name');
|
||||
});
|
||||
|
||||
it('Should create a library panel under a row', () => {
|
||||
@ -984,12 +966,13 @@ describe('DashboardScene', () => {
|
||||
|
||||
const layout = scene.state.body as SceneGridLayout;
|
||||
const newGridItem = (layout.state.children[0] as SceneGridRow).state.children[0] as DashboardGridItem;
|
||||
const behavior = newGridItem.state.body.state.$behaviors![0] as LibraryPanelBehavior;
|
||||
|
||||
expect(layout.state.children.length).toBe(1);
|
||||
expect((layout.state.children[0] as SceneGridRow).state.children.length).toBe(1);
|
||||
expect(newGridItem.state.body).toBeInstanceOf(LibraryVizPanel);
|
||||
expect((newGridItem.state.body as LibraryVizPanel).state.uid).toBe('uid');
|
||||
expect((newGridItem.state.body as LibraryVizPanel).state.name).toBe('name');
|
||||
expect(newGridItem.state.body).toBeInstanceOf(VizPanel);
|
||||
expect(behavior.state.uid).toBe('uid');
|
||||
expect(behavior.state.name).toBe('name');
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1266,16 +1249,11 @@ function buildTestScene(overrides?: Partial<DashboardSceneState>) {
|
||||
}),
|
||||
}),
|
||||
new DashboardGridItem({
|
||||
body: new LibraryVizPanel({
|
||||
uid: 'uid',
|
||||
name: 'libraryPanel',
|
||||
panelKey: 'panel-5',
|
||||
body: new VizPanel({
|
||||
title: 'Library Panel',
|
||||
panel: new VizPanel({
|
||||
title: 'Library Panel',
|
||||
key: 'panel-5',
|
||||
pluginId: 'table',
|
||||
}),
|
||||
pluginId: 'table',
|
||||
key: 'panel-5',
|
||||
$behaviors: [new LibraryPanelBehavior({ title: 'Library Panel', name: 'libraryPanel', uid: 'uid' })],
|
||||
}),
|
||||
}),
|
||||
],
|
||||
@ -1289,16 +1267,11 @@ function buildTestScene(overrides?: Partial<DashboardSceneState>) {
|
||||
}),
|
||||
}),
|
||||
new DashboardGridItem({
|
||||
body: new LibraryVizPanel({
|
||||
uid: 'uid',
|
||||
name: 'libraryPanel',
|
||||
panelKey: 'panel-6',
|
||||
body: new VizPanel({
|
||||
title: 'Library Panel',
|
||||
panel: new VizPanel({
|
||||
title: 'Library Panel',
|
||||
key: 'panel-6',
|
||||
pluginId: 'table',
|
||||
}),
|
||||
pluginId: 'table',
|
||||
key: 'panel-6',
|
||||
$behaviors: [new LibraryPanelBehavior({ title: 'Library Panel', name: 'libraryPanel', uid: 'uid' })],
|
||||
}),
|
||||
}),
|
||||
],
|
||||
|
@ -42,17 +42,13 @@ import { ShowConfirmModalEvent } from 'app/types/events';
|
||||
import { PanelEditor } from '../panel-edit/PanelEditor';
|
||||
import { DashboardSceneChangeTracker } from '../saving/DashboardSceneChangeTracker';
|
||||
import { SaveDashboardDrawer } from '../saving/SaveDashboardDrawer';
|
||||
import {
|
||||
buildGridItemForLibPanel,
|
||||
buildGridItemForPanel,
|
||||
transformSaveModelToScene,
|
||||
} from '../serialization/transformSaveModelToScene';
|
||||
import { buildGridItemForPanel, transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
|
||||
import { gridItemToPanel } from '../serialization/transformSceneToSaveModel';
|
||||
import { DecoratedRevisionModel } from '../settings/VersionsEditView';
|
||||
import { DashboardEditView } from '../settings/utils';
|
||||
import { historySrv } from '../settings/version-history';
|
||||
import { DashboardModelCompatibilityWrapper } from '../utils/DashboardModelCompatibilityWrapper';
|
||||
import { dashboardSceneGraph, getLibraryVizPanelFromVizPanel } from '../utils/dashboardSceneGraph';
|
||||
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
|
||||
import { djb2Hash } from '../utils/djb2Hash';
|
||||
import { getDashboardUrl, getViewPanelUrl } from '../utils/urlBuilders';
|
||||
import {
|
||||
@ -73,7 +69,7 @@ import { DashboardControls } from './DashboardControls';
|
||||
import { DashboardGridItem } from './DashboardGridItem';
|
||||
import { DashboardSceneRenderer } from './DashboardSceneRenderer';
|
||||
import { DashboardSceneUrlSync } from './DashboardSceneUrlSync';
|
||||
import { LibraryVizPanel } from './LibraryVizPanel';
|
||||
import { LibraryPanelBehavior } from './LibraryPanelBehavior';
|
||||
import { RowRepeaterBehavior } from './RowRepeaterBehavior';
|
||||
import { ViewPanelScene } from './ViewPanelScene';
|
||||
import { setupKeyboardShortcuts } from './keyboardShortcuts';
|
||||
@ -541,13 +537,8 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
throw new Error('Trying to add a panel in a layout that is not SceneGridLayout');
|
||||
}
|
||||
|
||||
const panelKey = panelToReplace.state.key;
|
||||
|
||||
const body = new LibraryVizPanel({
|
||||
title: libPanel.model?.title ?? 'Panel',
|
||||
uid: libPanel.uid,
|
||||
name: libPanel.name,
|
||||
panelKey: panelKey ?? getVizPanelKeyForPanelId(dashboardSceneGraph.getNextPanelId(this)),
|
||||
const body = panelToReplace.clone({
|
||||
$behaviors: [new LibraryPanelBehavior({ uid: libPanel.uid, name: libPanel.name })],
|
||||
});
|
||||
|
||||
const gridItem = panelToReplace.parent;
|
||||
@ -564,9 +555,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
return;
|
||||
}
|
||||
|
||||
const libraryPanel = getLibraryVizPanelFromVizPanel(vizPanel);
|
||||
|
||||
const gridItem = libraryPanel ? libraryPanel.parent : vizPanel.parent;
|
||||
const gridItem = vizPanel.parent;
|
||||
|
||||
if (!(gridItem instanceof DashboardGridItem)) {
|
||||
console.error('Trying to duplicate a panel in a layout that is not DashboardGridItem');
|
||||
@ -578,42 +567,28 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
let newGridItem;
|
||||
const newPanelId = dashboardSceneGraph.getNextPanelId(this);
|
||||
|
||||
if (libraryPanel) {
|
||||
const gridItemToDuplicateState = sceneUtils.cloneSceneObjectState(gridItem.state);
|
||||
|
||||
newGridItem = new DashboardGridItem({
|
||||
x: gridItemToDuplicateState.x,
|
||||
y: gridItemToDuplicateState.y,
|
||||
width: gridItemToDuplicateState.width,
|
||||
height: gridItemToDuplicateState.height,
|
||||
body: new LibraryVizPanel({
|
||||
title: libraryPanel.state.title,
|
||||
uid: libraryPanel.state.uid,
|
||||
name: libraryPanel.state.name,
|
||||
panelKey: getVizPanelKeyForPanelId(newPanelId),
|
||||
}),
|
||||
});
|
||||
if (gridItem instanceof DashboardGridItem) {
|
||||
panelState = sceneUtils.cloneSceneObjectState(gridItem.state.body.state);
|
||||
panelData = sceneGraph.getData(gridItem.state.body).clone();
|
||||
} else {
|
||||
if (gridItem instanceof DashboardGridItem) {
|
||||
panelState = sceneUtils.cloneSceneObjectState(gridItem.state.body.state);
|
||||
panelData = sceneGraph.getData(gridItem.state.body).clone();
|
||||
} else {
|
||||
panelState = sceneUtils.cloneSceneObjectState(vizPanel.state);
|
||||
panelData = sceneGraph.getData(vizPanel).clone();
|
||||
}
|
||||
|
||||
// when we duplicate a panel we don't want to clone the alert state
|
||||
delete panelData.state.data?.alertState;
|
||||
|
||||
newGridItem = new DashboardGridItem({
|
||||
x: gridItem.state.x,
|
||||
y: gridItem.state.y,
|
||||
height: gridItem.state.height,
|
||||
width: gridItem.state.width,
|
||||
body: new VizPanel({ ...panelState, $data: panelData, key: getVizPanelKeyForPanelId(newPanelId) }),
|
||||
});
|
||||
panelState = sceneUtils.cloneSceneObjectState(vizPanel.state);
|
||||
panelData = sceneGraph.getData(vizPanel).clone();
|
||||
}
|
||||
|
||||
// when we duplicate a panel we don't want to clone the alert state
|
||||
delete panelData.state.data?.alertState;
|
||||
|
||||
newGridItem = new DashboardGridItem({
|
||||
x: gridItem.state.x,
|
||||
y: gridItem.state.y,
|
||||
height: gridItem.state.height,
|
||||
width: gridItem.state.width,
|
||||
variableName: gridItem.state.variableName,
|
||||
repeatDirection: gridItem.state.repeatDirection,
|
||||
maxPerRow: gridItem.state.maxPerRow,
|
||||
body: new VizPanel({ ...panelState, $data: panelData, key: getVizPanelKeyForPanelId(newPanelId) }),
|
||||
});
|
||||
|
||||
if (!(this.state.body instanceof SceneGridLayout)) {
|
||||
console.error('Trying to duplicate a panel in a layout that is not SceneGridLayout ');
|
||||
return;
|
||||
@ -645,16 +620,6 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
|
||||
let gridItem = vizPanel.parent;
|
||||
|
||||
if (vizPanel.parent instanceof LibraryVizPanel) {
|
||||
const libraryVizPanel = vizPanel.parent;
|
||||
|
||||
if (!libraryVizPanel.parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
gridItem = libraryVizPanel.parent;
|
||||
}
|
||||
|
||||
if (!(gridItem instanceof DashboardGridItem)) {
|
||||
console.error('Trying to copy a panel that is not DashboardGridItem child');
|
||||
throw new Error('Trying to copy a panel that is not DashboardGridItem child');
|
||||
@ -674,9 +639,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
const jsonData = store.get(LS_PANEL_COPY_KEY);
|
||||
const jsonObj = JSON.parse(jsonData);
|
||||
const panelModel = new PanelModel(jsonObj);
|
||||
const gridItem = !panelModel.libraryPanel
|
||||
? buildGridItemForPanel(panelModel)
|
||||
: buildGridItemForLibPanel(panelModel);
|
||||
const gridItem = buildGridItemForPanel(panelModel);
|
||||
|
||||
const sceneGridLayout = this.state.body;
|
||||
|
||||
@ -686,25 +649,9 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
|
||||
const panelId = dashboardSceneGraph.getNextPanelId(this);
|
||||
|
||||
if (gridItem instanceof DashboardGridItem && gridItem.state.body instanceof LibraryVizPanel) {
|
||||
const panelKey = getVizPanelKeyForPanelId(panelId);
|
||||
|
||||
gridItem.state.body.setState({ panelKey });
|
||||
|
||||
const vizPanel = gridItem.state.body.state.panel;
|
||||
|
||||
if (vizPanel instanceof VizPanel) {
|
||||
vizPanel.setState({ key: panelKey });
|
||||
}
|
||||
} else if (gridItem instanceof DashboardGridItem && gridItem.state.body) {
|
||||
gridItem.state.body.setState({
|
||||
key: getVizPanelKeyForPanelId(panelId),
|
||||
});
|
||||
} else if (gridItem instanceof DashboardGridItem) {
|
||||
gridItem.state.body.setState({
|
||||
key: getVizPanelKeyForPanelId(panelId),
|
||||
});
|
||||
}
|
||||
gridItem.state.body.setState({
|
||||
key: getVizPanelKeyForPanelId(panelId),
|
||||
});
|
||||
|
||||
gridItem.setState({
|
||||
height: NEW_PANEL_HEIGHT,
|
||||
@ -723,7 +670,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
|
||||
public removePanel(panel: VizPanel) {
|
||||
const panels: SceneObject[] = [];
|
||||
const key = panel.parent instanceof LibraryVizPanel ? panel.parent.parent?.state.key : panel.parent?.state.key;
|
||||
const key = panel.parent?.state.key;
|
||||
|
||||
if (!key) {
|
||||
return;
|
||||
@ -764,7 +711,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
}
|
||||
}
|
||||
|
||||
public unlinkLibraryPanel(panel: LibraryVizPanel) {
|
||||
public unlinkLibraryPanel(panel: VizPanel) {
|
||||
if (!panel.parent) {
|
||||
return;
|
||||
}
|
||||
@ -772,13 +719,11 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
const gridItem = panel.parent;
|
||||
|
||||
if (!(gridItem instanceof DashboardGridItem)) {
|
||||
console.error('Trying to unlinka a lib panel in a layout that is not DashboardGridItem');
|
||||
console.error('Trying to unlink a lib panel in a layout that is not DashboardGridItem');
|
||||
return;
|
||||
}
|
||||
|
||||
gridItem?.setState({
|
||||
body: panel.state.panel?.clone(),
|
||||
});
|
||||
gridItem.state.body.setState({ $behaviors: undefined });
|
||||
}
|
||||
|
||||
public showModal(modal: SceneObject) {
|
||||
@ -812,7 +757,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
locationService.partial({ editview: 'settings' });
|
||||
};
|
||||
|
||||
public onShowAddLibraryPanelDrawer(panelToReplaceRef?: SceneObjectRef<LibraryVizPanel>) {
|
||||
public onShowAddLibraryPanelDrawer(panelToReplaceRef?: SceneObjectRef<VizPanel>) {
|
||||
this.setState({
|
||||
overlay: new AddLibraryPanelDrawer({ panelToReplaceRef }),
|
||||
});
|
||||
|
@ -21,13 +21,13 @@ import { ShareModal } from '../sharing/ShareModal';
|
||||
import {
|
||||
findVizPanelByKey,
|
||||
getDashboardSceneFor,
|
||||
getLibraryPanel,
|
||||
getLibraryPanelBehavior,
|
||||
isPanelClone,
|
||||
isWithinUnactivatedRepeatRow,
|
||||
} from '../utils/utils';
|
||||
|
||||
import { DashboardScene, DashboardSceneState } from './DashboardScene';
|
||||
import { LibraryVizPanel } from './LibraryVizPanel';
|
||||
import { LibraryPanelBehavior } from './LibraryPanelBehavior';
|
||||
import { ViewPanelScene } from './ViewPanelScene';
|
||||
import { DashboardRepeatsProcessedEvent } from './types';
|
||||
|
||||
@ -83,20 +83,6 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
if (getLibraryPanel(panel)) {
|
||||
this._handleLibraryPanel(panel, (p) => {
|
||||
if (p.state.key === undefined) {
|
||||
// Inspect drawer require a panel key to be set
|
||||
throw new Error('library panel key is undefined');
|
||||
}
|
||||
const drawer = new PanelInspectDrawer({
|
||||
$behaviors: [new ResolveInspectPanelByKey({ panelKey: p.state.key })],
|
||||
});
|
||||
this._scene.setState({ overlay: drawer, inspectPanelKey: p.state.key });
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
update.inspectPanelKey = values.inspect;
|
||||
update.overlay = new PanelInspectDrawer({
|
||||
$behaviors: [new ResolveInspectPanelByKey({ panelKey: values.inspect })],
|
||||
@ -122,11 +108,6 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
if (getLibraryPanel(panel)) {
|
||||
this._handleLibraryPanel(panel, (p) => this._buildLibraryPanelViewScene(p));
|
||||
return;
|
||||
}
|
||||
|
||||
if (isWithinUnactivatedRepeatRow(panel)) {
|
||||
this._handleViewRepeatClone(values.viewPanel);
|
||||
return;
|
||||
@ -155,10 +136,10 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
|
||||
if (!isEditing) {
|
||||
this._scene.onEnterEditMode();
|
||||
}
|
||||
if (getLibraryPanel(panel)) {
|
||||
this._handleLibraryPanel(panel, (p) => {
|
||||
this._scene.setState({ editPanel: buildPanelEditScene(p) });
|
||||
});
|
||||
|
||||
const libPanelBehavior = getLibraryPanelBehavior(panel);
|
||||
if (libPanelBehavior && !libPanelBehavior?.state.isLoaded) {
|
||||
this._waitForLibPanelToLoadBeforeEnteringPanelEdit(panel, libPanelBehavior);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -202,25 +183,6 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private _buildLibraryPanelViewScene(vizPanel: VizPanel) {
|
||||
this._scene.setState({ viewPanelScene: new ViewPanelScene({ panelRef: vizPanel.getRef() }) });
|
||||
}
|
||||
|
||||
private _handleLibraryPanel(vizPanel: VizPanel, cb: (p: VizPanel) => void): void {
|
||||
if (!(vizPanel.parent instanceof LibraryVizPanel)) {
|
||||
throw new Error('Panel is not a child of a LibraryVizPanel');
|
||||
}
|
||||
const libraryPanel = vizPanel.parent;
|
||||
if (libraryPanel.state.isLoaded) {
|
||||
cb(vizPanel);
|
||||
} else {
|
||||
libraryPanel.subscribeToState((n) => {
|
||||
cb(n.panel!);
|
||||
});
|
||||
libraryPanel.activate();
|
||||
}
|
||||
}
|
||||
|
||||
private _handleViewRepeatClone(viewPanel: string) {
|
||||
if (!this._eventSub) {
|
||||
this._eventSub = this._scene.subscribeToEvent(DashboardRepeatsProcessedEvent, () => {
|
||||
@ -232,6 +194,18 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporary solution, with some refactoring of PanelEditor we can remove this
|
||||
*/
|
||||
private _waitForLibPanelToLoadBeforeEnteringPanelEdit(panel: VizPanel, libPanel: LibraryPanelBehavior) {
|
||||
const sub = libPanel.subscribeToState((state) => {
|
||||
if (state.isLoaded) {
|
||||
this._scene.setState({ editPanel: buildPanelEditScene(panel) });
|
||||
sub.unsubscribe();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
interface ResolveInspectPanelByKeyState extends SceneObjectState {
|
||||
|
@ -0,0 +1,183 @@
|
||||
import { of } from 'rxjs';
|
||||
|
||||
import { FieldType, LoadingState, PanelData, getDefaultTimeRange, toDataFrame } from '@grafana/data';
|
||||
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
|
||||
import { setPluginImportUtils, setRunRequest } from '@grafana/runtime';
|
||||
import { SceneCanvasText, SceneGridLayout, VizPanel } from '@grafana/scenes';
|
||||
import { LibraryPanel } from '@grafana/schema';
|
||||
import * as libpanels from 'app/features/library-panels/state/api';
|
||||
|
||||
import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel';
|
||||
import { activateFullSceneTree } from '../utils/test-utils';
|
||||
|
||||
import { DashboardGridItem } from './DashboardGridItem';
|
||||
import { DashboardScene } from './DashboardScene';
|
||||
import { LibraryPanelBehavior } from './LibraryPanelBehavior';
|
||||
|
||||
setPluginImportUtils({
|
||||
importPanelPlugin: (id: string) => Promise.resolve(getPanelPlugin({})),
|
||||
getPanelPluginFromCache: (id: string) => undefined,
|
||||
});
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
setPluginExtensionGetter: jest.fn(),
|
||||
getPluginLinkExtensions: jest.fn(() => ({
|
||||
extensions: [],
|
||||
})),
|
||||
getDataSourceSrv: () => {
|
||||
return {
|
||||
get: jest.fn().mockResolvedValue({
|
||||
getRef: () => ({ uid: 'ds1' }),
|
||||
}),
|
||||
getInstanceSettings: jest.fn().mockResolvedValue({ uid: 'ds1' }),
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
const runRequestMock = jest.fn().mockReturnValue(
|
||||
of<PanelData>({
|
||||
state: LoadingState.Done,
|
||||
timeRange: getDefaultTimeRange(),
|
||||
series: [
|
||||
toDataFrame({
|
||||
fields: [{ name: 'value', type: FieldType.number, values: [1, 2, 3] }],
|
||||
}),
|
||||
],
|
||||
request: {
|
||||
app: 'dashboard',
|
||||
requestId: 'request-id',
|
||||
dashboardUID: 'asd',
|
||||
interval: '1s',
|
||||
panelId: 1,
|
||||
range: getDefaultTimeRange(),
|
||||
targets: [],
|
||||
timezone: 'utc',
|
||||
intervalMs: 1000,
|
||||
startTime: 1,
|
||||
scopedVars: {
|
||||
__sceneObject: { value: new SceneCanvasText({ text: 'asd' }) },
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
setRunRequest(runRequestMock);
|
||||
|
||||
describe('LibraryPanelBehavior', () => {
|
||||
it('should load library panel', async () => {
|
||||
const { gridItem, spy, behavior } = await buildTestSceneWithLibraryPanel();
|
||||
|
||||
expect(behavior.state.isLoaded).toBe(true);
|
||||
expect(behavior.state._loadedPanel).toBeDefined();
|
||||
expect(behavior.state._loadedPanel?.model).toBeDefined();
|
||||
expect(behavior.state._loadedPanel?.name).toBe('LibraryPanel A');
|
||||
expect(behavior.state._loadedPanel?.type).toBe('table');
|
||||
|
||||
// Verify the viz panel state have been updated with lib panel options
|
||||
expect(gridItem.state.body.state.options).toEqual({ showHeader: true });
|
||||
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not update panel if version is the same', async () => {
|
||||
const { gridItem } = await buildTestSceneWithLibraryPanel();
|
||||
|
||||
const behavior = gridItem.state.body.state.$behaviors![0] as LibraryPanelBehavior;
|
||||
expect(behavior).toBeDefined();
|
||||
|
||||
const panel = vizPanelToPanel(gridItem.state.body.clone({ $behaviors: undefined }));
|
||||
|
||||
const libraryPanelState = {
|
||||
name: 'LibraryPanel B',
|
||||
title: 'LibraryPanel B title',
|
||||
uid: '222',
|
||||
type: 'table',
|
||||
version: 1,
|
||||
model: panel,
|
||||
};
|
||||
|
||||
behavior.setPanelFromLibPanel(libraryPanelState);
|
||||
|
||||
expect(behavior.state._loadedPanel?.name).toBe('LibraryPanel A');
|
||||
expect(behavior.state._loadedPanel?.uid).toBe('111');
|
||||
});
|
||||
|
||||
it('should not update panel if behavior not part of a vizPanel', async () => {
|
||||
const { gridItem } = await buildTestSceneWithLibraryPanel();
|
||||
|
||||
const behavior = gridItem.state.body.state.$behaviors![0] as LibraryPanelBehavior;
|
||||
expect(behavior).toBeDefined();
|
||||
|
||||
const panel = vizPanelToPanel(gridItem.state.body.clone({ $behaviors: undefined }));
|
||||
|
||||
const libraryPanelState = {
|
||||
name: 'LibraryPanel B',
|
||||
title: 'LibraryPanel B title',
|
||||
uid: '222',
|
||||
type: 'table',
|
||||
version: 2,
|
||||
model: panel,
|
||||
};
|
||||
|
||||
const behaviorClone = behavior.clone();
|
||||
behaviorClone.setPanelFromLibPanel(libraryPanelState);
|
||||
|
||||
expect(behaviorClone.state._loadedPanel?.name).toBe('LibraryPanel A');
|
||||
expect(behaviorClone.state._loadedPanel?.uid).toBe('111');
|
||||
});
|
||||
});
|
||||
|
||||
async function buildTestSceneWithLibraryPanel() {
|
||||
const behavior = new LibraryPanelBehavior({ title: 'LibraryPanel A title', name: 'LibraryPanel A', uid: '111' });
|
||||
|
||||
const vizPanel = new VizPanel({
|
||||
title: 'Panel A',
|
||||
pluginId: 'lib-panel-loading',
|
||||
key: 'panel-1',
|
||||
$behaviors: [behavior],
|
||||
});
|
||||
|
||||
const libraryPanel: LibraryPanel = {
|
||||
name: 'LibraryPanel A',
|
||||
uid: '111',
|
||||
type: 'table',
|
||||
model: {
|
||||
title: 'LibraryPanel A title',
|
||||
type: 'table',
|
||||
options: { showHeader: true },
|
||||
fieldConfig: { defaults: {}, overrides: [] },
|
||||
datasource: { uid: 'abcdef' },
|
||||
targets: [{ refId: 'A' }],
|
||||
},
|
||||
version: 1,
|
||||
};
|
||||
|
||||
const spy = jest.spyOn(libpanels, 'getLibraryPanel').mockResolvedValue(libraryPanel);
|
||||
|
||||
const gridItem = new DashboardGridItem({
|
||||
key: 'griditem-1',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 10,
|
||||
height: 12,
|
||||
body: vizPanel,
|
||||
});
|
||||
|
||||
const scene = new DashboardScene({
|
||||
title: 'hello',
|
||||
uid: 'dash-1',
|
||||
meta: {
|
||||
canEdit: true,
|
||||
},
|
||||
body: new SceneGridLayout({
|
||||
children: [gridItem],
|
||||
}),
|
||||
});
|
||||
|
||||
activateFullSceneTree(scene);
|
||||
|
||||
await new Promise((r) => setTimeout(r, 1));
|
||||
|
||||
return { scene, gridItem, spy, behavior };
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
import { PanelPlugin, PanelProps } from '@grafana/data';
|
||||
import { SceneObjectBase, SceneObjectState, sceneUtils, VizPanel, VizPanelState } from '@grafana/scenes';
|
||||
import { LibraryPanel } from '@grafana/schema';
|
||||
import { Stack } from '@grafana/ui';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
import { PanelModel } from 'app/features/dashboard/state';
|
||||
import { getLibraryPanel } from 'app/features/library-panels/state/api';
|
||||
|
||||
import { createPanelDataProvider } from '../utils/createPanelDataProvider';
|
||||
|
||||
import { DashboardGridItem } from './DashboardGridItem';
|
||||
|
||||
interface LibraryPanelBehaviorState extends SceneObjectState {
|
||||
// Library panels use title from dashboard JSON's panel model, not from library panel definition, hence we pass it.
|
||||
title?: string;
|
||||
uid: string;
|
||||
name: string;
|
||||
isLoaded?: boolean;
|
||||
_loadedPanel?: LibraryPanel;
|
||||
}
|
||||
|
||||
export class LibraryPanelBehavior extends SceneObjectBase<LibraryPanelBehaviorState> {
|
||||
public static LOADING_VIZ_PANEL_PLUGIN_ID = 'library-panel-loading-plugin';
|
||||
|
||||
public constructor(state: LibraryPanelBehaviorState) {
|
||||
super(state);
|
||||
|
||||
this.addActivationHandler(() => this._activationHandler());
|
||||
}
|
||||
|
||||
private _activationHandler() {
|
||||
if (!this.state.isLoaded) {
|
||||
this.loadLibraryPanelFromPanelModel();
|
||||
}
|
||||
}
|
||||
|
||||
public setPanelFromLibPanel(libPanel: LibraryPanel) {
|
||||
if (this.state._loadedPanel?.version === libPanel.version) {
|
||||
return;
|
||||
}
|
||||
|
||||
const vizPanel = this.parent;
|
||||
|
||||
if (!(vizPanel instanceof VizPanel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const libPanelModel = new PanelModel(libPanel.model);
|
||||
|
||||
const vizPanelState: VizPanelState = {
|
||||
title: libPanelModel.title,
|
||||
options: libPanelModel.options ?? {},
|
||||
fieldConfig: libPanelModel.fieldConfig,
|
||||
pluginId: libPanelModel.type,
|
||||
pluginVersion: libPanelModel.pluginVersion,
|
||||
displayMode: libPanelModel.transparent ? 'transparent' : undefined,
|
||||
description: libPanelModel.description,
|
||||
$data: createPanelDataProvider(libPanelModel),
|
||||
};
|
||||
|
||||
vizPanel.setState(vizPanelState);
|
||||
vizPanel.changePluginType(libPanelModel.type, vizPanelState.options, vizPanelState.fieldConfig);
|
||||
|
||||
this.setState({ _loadedPanel: libPanel, isLoaded: true, name: libPanel.name, title: libPanelModel.title });
|
||||
|
||||
const layoutElement = vizPanel.parent!;
|
||||
|
||||
// Migrate repeat options to layout element
|
||||
if (libPanelModel.repeat && layoutElement instanceof DashboardGridItem) {
|
||||
layoutElement.setState({
|
||||
variableName: libPanelModel.repeat,
|
||||
repeatDirection: libPanelModel.repeatDirection === 'h' ? 'h' : 'v',
|
||||
maxPerRow: libPanelModel.maxPerRow,
|
||||
itemHeight: layoutElement.state.height ?? 10,
|
||||
});
|
||||
layoutElement.performRepeat();
|
||||
}
|
||||
}
|
||||
|
||||
private async loadLibraryPanelFromPanelModel() {
|
||||
let vizPanel = this.parent;
|
||||
|
||||
if (!(vizPanel instanceof VizPanel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const libPanel = await getLibraryPanel(this.state.uid, true);
|
||||
this.setPanelFromLibPanel(libPanel);
|
||||
} catch (err) {
|
||||
vizPanel.setState({
|
||||
_pluginLoadError: `Unable to load library panel: ${this.state.uid}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const LoadingVizPanelPlugin = new PanelPlugin(LoadingVizPanel);
|
||||
|
||||
function LoadingVizPanel(props: PanelProps) {
|
||||
return (
|
||||
<Stack direction={'column'} justifyContent={'space-between'}>
|
||||
<Trans i18nKey="library-panels.loading-panel-text">Loading library panel</Trans>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
sceneUtils.registerRuntimePanelPlugin({
|
||||
pluginId: LibraryPanelBehavior.LOADING_VIZ_PANEL_PLUGIN_ID,
|
||||
plugin: LoadingVizPanelPlugin,
|
||||
});
|
@ -1,140 +0,0 @@
|
||||
import { waitFor } from '@testing-library/dom';
|
||||
import { merge } from 'lodash';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { setupServer, SetupServerApi } from 'msw/node';
|
||||
|
||||
import { setBackendSrv } from '@grafana/runtime';
|
||||
import { SceneGridLayout, VizPanel } from '@grafana/scenes';
|
||||
import { LibraryPanel } from '@grafana/schema';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
|
||||
import { DashboardGridItem } from './DashboardGridItem';
|
||||
import { LibraryVizPanel } from './LibraryVizPanel';
|
||||
|
||||
describe('LibraryVizPanel', () => {
|
||||
const server = setupServer();
|
||||
|
||||
beforeAll(() => {
|
||||
setBackendSrv(backendSrv);
|
||||
server.listen();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
server.close();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
it('should fetch and init', async () => {
|
||||
setUpApiMock(server);
|
||||
const libVizPanel = new LibraryVizPanel({
|
||||
name: 'My Library Panel',
|
||||
title: 'Panel title',
|
||||
uid: 'fdcvggvfy2qdca',
|
||||
panelKey: 'lib-panel',
|
||||
});
|
||||
libVizPanel.activate();
|
||||
await waitFor(() => {
|
||||
expect(libVizPanel.state.panel).toBeInstanceOf(VizPanel);
|
||||
});
|
||||
});
|
||||
|
||||
it('should configure repeat options of DashboardGridIem if repeat is set', async () => {
|
||||
setUpApiMock(server, { model: { repeat: 'query0', repeatDirection: 'h' } });
|
||||
const libVizPanel = new LibraryVizPanel({
|
||||
name: 'My Library Panel',
|
||||
title: 'Panel title',
|
||||
uid: 'fdcvggvfy2qdca',
|
||||
panelKey: 'lib-panel',
|
||||
});
|
||||
|
||||
const layout = new SceneGridLayout({
|
||||
children: [new DashboardGridItem({ body: libVizPanel })],
|
||||
});
|
||||
|
||||
layout.activate();
|
||||
libVizPanel.activate();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(layout.state.children[0]).toBeInstanceOf(DashboardGridItem);
|
||||
expect((layout.state.children[0] as DashboardGridItem).state.variableName).toBe('query0');
|
||||
expect((layout.state.children[0] as DashboardGridItem).state.repeatDirection).toBe('h');
|
||||
expect((layout.state.children[0] as DashboardGridItem).state.maxPerRow).toBe(4);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function setUpApiMock(
|
||||
server: SetupServerApi,
|
||||
overrides: Omit<Partial<LibraryPanel>, 'model'> & { model?: Partial<LibraryPanel['model']> } = {}
|
||||
) {
|
||||
const libPanel: LibraryPanel = merge(
|
||||
{
|
||||
folderUid: 'general',
|
||||
uid: 'fdcvggvfy2qdca',
|
||||
name: 'My Library Panel',
|
||||
type: 'timeseries',
|
||||
description: '',
|
||||
model: {
|
||||
datasource: {
|
||||
type: 'grafana-testdata-datasource',
|
||||
uid: 'PD8C576611E62080A',
|
||||
},
|
||||
description: '',
|
||||
|
||||
maxPerRow: 4,
|
||||
options: {
|
||||
legend: {
|
||||
calcs: [],
|
||||
displayMode: 'list',
|
||||
placement: 'bottom',
|
||||
showLegend: true,
|
||||
},
|
||||
tooltip: {
|
||||
maxHeight: 600,
|
||||
mode: 'single',
|
||||
sort: 'none',
|
||||
},
|
||||
},
|
||||
targets: [
|
||||
{
|
||||
datasource: {
|
||||
type: 'grafana-testdata-datasource',
|
||||
uid: 'PD8C576611E62080A',
|
||||
},
|
||||
refId: 'A',
|
||||
},
|
||||
],
|
||||
title: 'Panel Title',
|
||||
type: 'timeseries',
|
||||
},
|
||||
version: 6,
|
||||
meta: {
|
||||
folderName: 'Dashboards',
|
||||
folderUid: '',
|
||||
connectedDashboards: 1,
|
||||
created: '2024-02-15T15:26:46Z',
|
||||
updated: '2024-02-28T15:54:22Z',
|
||||
createdBy: {
|
||||
avatarUrl: '/avatar/46d229b033af06a191ff2267bca9ae56',
|
||||
id: 1,
|
||||
name: 'admin',
|
||||
},
|
||||
updatedBy: {
|
||||
avatarUrl: '/avatar/46d229b033af06a191ff2267bca9ae56',
|
||||
id: 1,
|
||||
name: 'admin',
|
||||
},
|
||||
},
|
||||
},
|
||||
overrides
|
||||
);
|
||||
|
||||
const libPanelMock: { result: LibraryPanel } = {
|
||||
result: libPanel,
|
||||
};
|
||||
|
||||
server.use(http.get('/api/library-elements/:uid', () => HttpResponse.json(libPanelMock)));
|
||||
}
|
@ -1,124 +0,0 @@
|
||||
import {
|
||||
SceneComponentProps,
|
||||
SceneObjectBase,
|
||||
SceneObjectState,
|
||||
VizPanel,
|
||||
VizPanelMenu,
|
||||
VizPanelState,
|
||||
} from '@grafana/scenes';
|
||||
import { LibraryPanel } from '@grafana/schema';
|
||||
import { PanelModel } from 'app/features/dashboard/state';
|
||||
import { getLibraryPanel } from 'app/features/library-panels/state/api';
|
||||
|
||||
import { createPanelDataProvider } from '../utils/createPanelDataProvider';
|
||||
|
||||
import { DashboardGridItem } from './DashboardGridItem';
|
||||
import { VizPanelLinks, VizPanelLinksMenu } from './PanelLinks';
|
||||
import { panelLinksBehavior, panelMenuBehavior } from './PanelMenuBehavior';
|
||||
import { PanelNotices } from './PanelNotices';
|
||||
|
||||
export interface LibraryVizPanelState extends SceneObjectState {
|
||||
// Library panels use title from dashboard JSON's panel model, not from library panel definition, hence we pass it.
|
||||
title: string;
|
||||
uid: string;
|
||||
name: string;
|
||||
panel?: VizPanel;
|
||||
isLoaded?: boolean;
|
||||
panelKey: string;
|
||||
_loadedPanel?: LibraryPanel;
|
||||
}
|
||||
|
||||
export class LibraryVizPanel extends SceneObjectBase<LibraryVizPanelState> {
|
||||
static Component = LibraryPanelRenderer;
|
||||
|
||||
constructor(state: LibraryVizPanelState) {
|
||||
super({
|
||||
panel: state.panel ?? getLoadingPanel(state.title, state.panelKey),
|
||||
isLoaded: state.isLoaded ?? false,
|
||||
...state,
|
||||
});
|
||||
|
||||
this.addActivationHandler(this._onActivate);
|
||||
}
|
||||
|
||||
private _onActivate = () => {
|
||||
if (!this.state.isLoaded) {
|
||||
this.loadLibraryPanelFromPanelModel();
|
||||
}
|
||||
};
|
||||
|
||||
public setPanelFromLibPanel(libPanel: LibraryPanel) {
|
||||
if (this.state._loadedPanel?.version === libPanel.version) {
|
||||
return;
|
||||
}
|
||||
|
||||
const libPanelModel = new PanelModel(libPanel.model);
|
||||
|
||||
const vizPanelState: VizPanelState = {
|
||||
title: libPanelModel.title,
|
||||
key: this.state.panelKey,
|
||||
options: libPanelModel.options ?? {},
|
||||
fieldConfig: libPanelModel.fieldConfig,
|
||||
pluginId: libPanelModel.type,
|
||||
pluginVersion: libPanelModel.pluginVersion,
|
||||
displayMode: libPanelModel.transparent ? 'transparent' : undefined,
|
||||
description: libPanelModel.description,
|
||||
// To be replaced with it's own option persisted option instead derived
|
||||
hoverHeader: !libPanelModel.title && !libPanelModel.timeFrom && !libPanelModel.timeShift,
|
||||
hoverHeaderOffset: 0,
|
||||
$data: createPanelDataProvider(libPanelModel),
|
||||
menu: new VizPanelMenu({ $behaviors: [panelMenuBehavior] }),
|
||||
titleItems: [
|
||||
new VizPanelLinks({
|
||||
rawLinks: libPanelModel.links,
|
||||
menu: new VizPanelLinksMenu({ $behaviors: [panelLinksBehavior] }),
|
||||
}),
|
||||
new PanelNotices(),
|
||||
],
|
||||
};
|
||||
|
||||
const panel = new VizPanel(vizPanelState);
|
||||
|
||||
this.setState({ panel, _loadedPanel: libPanel, isLoaded: true, name: libPanel.name });
|
||||
}
|
||||
|
||||
private async loadLibraryPanelFromPanelModel() {
|
||||
let vizPanel = this.state.panel!;
|
||||
|
||||
try {
|
||||
const libPanel = await getLibraryPanel(this.state.uid, true);
|
||||
this.setPanelFromLibPanel(libPanel);
|
||||
if (this.parent instanceof DashboardGridItem) {
|
||||
this.parent.setState({
|
||||
variableName: libPanel.model.repeat,
|
||||
repeatDirection: libPanel.model.repeatDirection === 'h' ? 'h' : 'v',
|
||||
maxPerRow: libPanel.model.maxPerRow,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
vizPanel.setState({
|
||||
_pluginLoadError: `Unable to load library panel: ${this.state.uid}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getLoadingPanel(title: string, panelKey: string) {
|
||||
return new VizPanel({
|
||||
key: panelKey,
|
||||
title,
|
||||
menu: new VizPanelMenu({
|
||||
$behaviors: [panelMenuBehavior],
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function LibraryPanelRenderer({ model }: SceneComponentProps<LibraryVizPanel>) {
|
||||
const { panel } = model.useState();
|
||||
|
||||
if (!panel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <panel.Component model={panel} />;
|
||||
}
|
@ -30,10 +30,10 @@ import ExportButton from '../sharing/ExportButton/ExportButton';
|
||||
import ShareButton from '../sharing/ShareButton/ShareButton';
|
||||
import { DashboardInteractions } from '../utils/interactions';
|
||||
import { DynamicDashNavButtonModel, dynamicDashNavActions } from '../utils/registerDynamicDashNavAction';
|
||||
import { isLibraryPanel } from '../utils/utils';
|
||||
|
||||
import { DashboardScene } from './DashboardScene';
|
||||
import { GoToSnapshotOriginButton } from './GoToSnapshotOriginButton';
|
||||
import { LibraryVizPanel } from './LibraryVizPanel';
|
||||
|
||||
interface Props {
|
||||
dashboard: DashboardScene;
|
||||
@ -619,7 +619,7 @@ function useEditingLibraryPanel(panelEditor?: PanelEditor) {
|
||||
useEffect(() => {
|
||||
if (panelEditor) {
|
||||
const unsub = panelEditor.state.vizManager.subscribeToState((vizManagerState) =>
|
||||
setEditingLibraryPanel(vizManagerState.sourcePanel.resolve().parent instanceof LibraryVizPanel)
|
||||
setEditingLibraryPanel(isLibraryPanel(vizManagerState.sourcePanel.resolve()))
|
||||
);
|
||||
return () => {
|
||||
unsub.unsubscribe();
|
||||
|
@ -26,10 +26,9 @@ import { ShareDrawer } from '../sharing/ShareDrawer/ShareDrawer';
|
||||
import { ShareModal } from '../sharing/ShareModal';
|
||||
import { DashboardInteractions } from '../utils/interactions';
|
||||
import { getEditPanelUrl, getInspectUrl, getViewPanelUrl, tryGetExploreUrlForPanel } from '../utils/urlBuilders';
|
||||
import { getDashboardSceneFor, getPanelIdForVizPanel, getQueryRunnerFor } from '../utils/utils';
|
||||
import { getDashboardSceneFor, getPanelIdForVizPanel, getQueryRunnerFor, isLibraryPanel } from '../utils/utils';
|
||||
|
||||
import { DashboardScene } from './DashboardScene';
|
||||
import { LibraryVizPanel } from './LibraryVizPanel';
|
||||
import { VizPanelLinks, VizPanelLinksMenu } from './PanelLinks';
|
||||
import { UnlinkLibraryPanelModal } from './UnlinkLibraryPanelModal';
|
||||
|
||||
@ -41,7 +40,6 @@ export function panelMenuBehavior(menu: VizPanelMenu, isRepeat = false) {
|
||||
// hm.. add another generic param to SceneObject to specify parent type?
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
const panel = menu.parent as VizPanel;
|
||||
const parent = panel.parent;
|
||||
const plugin = panel.getPlugin();
|
||||
|
||||
const items: PanelMenuItem[] = [];
|
||||
@ -164,13 +162,13 @@ export function panelMenuBehavior(menu: VizPanelMenu, isRepeat = false) {
|
||||
}
|
||||
|
||||
if (dashboard.state.isEditing && !isRepeat && !isEditingPanel) {
|
||||
if (parent instanceof LibraryVizPanel) {
|
||||
if (isLibraryPanel(panel)) {
|
||||
moreSubMenu.push({
|
||||
text: t('panel.header-menu.unlink-library-panel', `Unlink library panel`),
|
||||
onClick: () => {
|
||||
dashboard.showModal(
|
||||
new UnlinkLibraryPanelModal({
|
||||
panelRef: parent.getRef(),
|
||||
panelRef: panel.getRef(),
|
||||
})
|
||||
);
|
||||
},
|
||||
@ -179,7 +177,7 @@ export function panelMenuBehavior(menu: VizPanelMenu, isRepeat = false) {
|
||||
moreSubMenu.push({
|
||||
text: t('panel.header-menu.replace-library-panel', `Replace library panel`),
|
||||
onClick: () => {
|
||||
dashboard.onShowAddLibraryPanelDrawer(parent.getRef());
|
||||
dashboard.onShowAddLibraryPanelDrawer(panel.getRef());
|
||||
},
|
||||
});
|
||||
} else {
|
||||
|
@ -1,13 +1,12 @@
|
||||
import { SceneComponentProps, SceneObjectBase, SceneObjectRef, SceneObjectState } from '@grafana/scenes';
|
||||
import { SceneComponentProps, SceneObjectBase, SceneObjectRef, SceneObjectState, VizPanel } from '@grafana/scenes';
|
||||
|
||||
import { ModalSceneObjectLike } from '../sharing/types';
|
||||
import { getDashboardSceneFor } from '../utils/utils';
|
||||
|
||||
import { LibraryVizPanel } from './LibraryVizPanel';
|
||||
import { UnlinkModal } from './UnlinkModal';
|
||||
|
||||
interface UnlinkLibraryPanelModalState extends SceneObjectState {
|
||||
panelRef?: SceneObjectRef<LibraryVizPanel>;
|
||||
panelRef?: SceneObjectRef<VizPanel>;
|
||||
}
|
||||
|
||||
export class UnlinkLibraryPanelModal
|
||||
|
@ -28,7 +28,7 @@ import { DashboardDataDTO } from 'app/types';
|
||||
|
||||
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
|
||||
import { DashboardGridItem } from '../scene/DashboardGridItem';
|
||||
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
|
||||
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
|
||||
import { PanelTimeRange } from '../scene/PanelTimeRange';
|
||||
import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior';
|
||||
import { NEW_LINK } from '../settings/links/utils';
|
||||
@ -44,7 +44,6 @@ import {
|
||||
buildGridItemForPanel,
|
||||
transformSaveModelToScene,
|
||||
convertOldSnapshotToScenesSnapshot,
|
||||
buildGridItemForLibPanel,
|
||||
} from './transformSaveModelToScene';
|
||||
|
||||
describe('transformSaveModelToScene', () => {
|
||||
@ -388,7 +387,9 @@ describe('transformSaveModelToScene', () => {
|
||||
expect(rowScene.state.children[0]).toBeInstanceOf(DashboardGridItem);
|
||||
expect(rowScene.state.children[1]).toBeInstanceOf(DashboardGridItem);
|
||||
// Panels are sorted by position in the row
|
||||
expect((rowScene.state.children[0] as DashboardGridItem).state.body!).toBeInstanceOf(LibraryVizPanel);
|
||||
expect((rowScene.state.children[0] as DashboardGridItem).state.body.state.$behaviors![0]).toBeInstanceOf(
|
||||
LibraryPanelBehavior
|
||||
);
|
||||
expect((rowScene.state.children[1] as DashboardGridItem).state.body!).toBeInstanceOf(VizPanel);
|
||||
});
|
||||
|
||||
@ -481,7 +482,7 @@ describe('transformSaveModelToScene', () => {
|
||||
// lib panel out of row
|
||||
expect(body.state.children[1]).toBeInstanceOf(DashboardGridItem);
|
||||
const panelOutOfRowLibVizPanel = body.state.children[1] as DashboardGridItem;
|
||||
expect(panelOutOfRowLibVizPanel.state.body!).toBeInstanceOf(LibraryVizPanel);
|
||||
expect(panelOutOfRowLibVizPanel.state.body.state.$behaviors![0]).toBeInstanceOf(LibraryPanelBehavior);
|
||||
// Row with panels
|
||||
expect(body.state.children[2]).toBeInstanceOf(SceneGridRow);
|
||||
const rowWithPanelsScene = body.state.children[2] as SceneGridRow;
|
||||
@ -489,7 +490,7 @@ describe('transformSaveModelToScene', () => {
|
||||
expect(rowWithPanelsScene.state.key).toBe('panel-10');
|
||||
expect(rowWithPanelsScene.state.children).toHaveLength(2);
|
||||
const libPanel = rowWithPanelsScene.state.children[1] as DashboardGridItem;
|
||||
expect(libPanel.state.body!).toBeInstanceOf(LibraryVizPanel);
|
||||
expect(libPanel.state.body.state.$behaviors![0]).toBeInstanceOf(LibraryPanelBehavior);
|
||||
// Panel within row
|
||||
expect(rowWithPanelsScene.state.children[0]).toBeInstanceOf(DashboardGridItem);
|
||||
const panelInRowVizPanel = rowWithPanelsScene.state.children[0] as DashboardGridItem;
|
||||
@ -721,7 +722,7 @@ describe('transformSaveModelToScene', () => {
|
||||
expect(runner.state.queryCachingTTL).toBe(200000);
|
||||
});
|
||||
|
||||
it('should convert saved lib panel to LibraryVizPanel', () => {
|
||||
it('should convert saved lib panel to a viz panel with LibraryPanelBehavior', () => {
|
||||
const panel = {
|
||||
title: 'Panel',
|
||||
gridPos: { x: 0, y: 0, w: 12, h: 8 },
|
||||
@ -733,12 +734,13 @@ describe('transformSaveModelToScene', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const gridItem = buildGridItemForLibPanel(new PanelModel(panel))!;
|
||||
const libVizPanel = gridItem.state.body as LibraryVizPanel;
|
||||
const gridItem = buildGridItemForPanel(new PanelModel(panel))!;
|
||||
const libPanelBehavior = gridItem.state.body.state.$behaviors![0];
|
||||
|
||||
expect(libVizPanel.state.uid).toEqual(panel.libraryPanel.uid);
|
||||
expect(libVizPanel.state.name).toEqual(panel.libraryPanel.name);
|
||||
expect(libVizPanel.state.title).toEqual(panel.title);
|
||||
expect(libPanelBehavior).toBeInstanceOf(LibraryPanelBehavior);
|
||||
expect((libPanelBehavior as LibraryPanelBehavior).state.uid).toEqual(panel.libraryPanel.uid);
|
||||
expect((libPanelBehavior as LibraryPanelBehavior).state.name).toEqual(panel.libraryPanel.name);
|
||||
expect(gridItem.state.body.state.title).toEqual(panel.title);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -32,7 +32,7 @@ import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
|
||||
import { DashboardGridItem, RepeatDirection } from '../scene/DashboardGridItem';
|
||||
import { registerDashboardMacro } from '../scene/DashboardMacro';
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
|
||||
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
|
||||
import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks';
|
||||
import { panelLinksBehavior, panelMenuBehavior } from '../scene/PanelMenuBehavior';
|
||||
import { PanelNotices } from '../scene/PanelNotices';
|
||||
@ -107,18 +107,6 @@ export function createSceneObjectsForPanels(oldPanels: PanelModel[]): SceneGridI
|
||||
currentRowPanels = [];
|
||||
}
|
||||
}
|
||||
} else if (panel.libraryPanel?.uid && !('model' in panel.libraryPanel)) {
|
||||
const gridItem = buildGridItemForLibPanel(panel);
|
||||
|
||||
if (!gridItem) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentRow) {
|
||||
currentRowPanels.push(gridItem);
|
||||
} else {
|
||||
panels.push(gridItem);
|
||||
}
|
||||
} else {
|
||||
// when rendering a snapshot created with the legacy Dashboards convert data to new snapshot format to be compatible with Scenes
|
||||
if (panel.snapshotData) {
|
||||
@ -153,16 +141,6 @@ function createRowFromPanelModel(row: PanelModel, content: SceneGridItemLike[]):
|
||||
saveModel = new PanelModel(saveModel);
|
||||
}
|
||||
|
||||
if (saveModel.libraryPanel?.uid && !('model' in saveModel.libraryPanel)) {
|
||||
const gridItem = buildGridItemForLibPanel(saveModel);
|
||||
|
||||
if (!gridItem) {
|
||||
throw new Error('Failed to build grid item for library panel');
|
||||
}
|
||||
|
||||
return gridItem;
|
||||
}
|
||||
|
||||
return buildGridItemForPanel(saveModel);
|
||||
});
|
||||
}
|
||||
@ -287,29 +265,6 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel,
|
||||
return dashboardScene;
|
||||
}
|
||||
|
||||
export function buildGridItemForLibPanel(panel: PanelModel) {
|
||||
if (!panel.libraryPanel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const body = new LibraryVizPanel({
|
||||
title: panel.title,
|
||||
uid: panel.libraryPanel.uid,
|
||||
name: panel.libraryPanel.name,
|
||||
panelKey: getVizPanelKeyForPanelId(panel.id),
|
||||
});
|
||||
|
||||
return new DashboardGridItem({
|
||||
key: `grid-item-${panel.id}`,
|
||||
y: panel.gridPos.y,
|
||||
x: panel.gridPos.x,
|
||||
width: panel.gridPos.w,
|
||||
height: panel.gridPos.h,
|
||||
itemHeight: panel.gridPos.h,
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
export function buildGridItemForPanel(panel: PanelModel): DashboardGridItem {
|
||||
const repeatOptions: Partial<{ variableName: string; repeatDirection: RepeatDirection }> = panel.repeat
|
||||
? {
|
||||
@ -333,7 +288,7 @@ export function buildGridItemForPanel(panel: PanelModel): DashboardGridItem {
|
||||
key: getVizPanelKeyForPanelId(panel.id),
|
||||
title: panel.title,
|
||||
description: panel.description,
|
||||
pluginId: panel.type,
|
||||
pluginId: panel.type ?? 'timeseries',
|
||||
options: panel.options ?? {},
|
||||
fieldConfig: panel.fieldConfig,
|
||||
pluginVersion: panel.pluginVersion,
|
||||
@ -343,11 +298,19 @@ export function buildGridItemForPanel(panel: PanelModel): DashboardGridItem {
|
||||
hoverHeaderOffset: 0,
|
||||
$data: createPanelDataProvider(panel),
|
||||
titleItems,
|
||||
|
||||
$behaviors: [],
|
||||
extendPanelContext: setDashboardPanelContext,
|
||||
_UNSAFE_customMigrationHandler: getAngularPanelMigrationHandler(panel),
|
||||
};
|
||||
|
||||
if (panel.libraryPanel) {
|
||||
vizPanelState.$behaviors!.push(
|
||||
new LibraryPanelBehavior({ uid: panel.libraryPanel.uid, name: panel.libraryPanel.name })
|
||||
);
|
||||
vizPanelState.pluginId = LibraryPanelBehavior.LOADING_VIZ_PANEL_PLUGIN_ID;
|
||||
vizPanelState.$data = undefined;
|
||||
}
|
||||
|
||||
if (!config.publicDashboardAccessToken) {
|
||||
vizPanelState.menu = new VizPanelMenu({
|
||||
$behaviors: [panelMenuBehavior],
|
||||
|
@ -33,7 +33,7 @@ import { buildPanelEditScene } from '../panel-edit/PanelEditor';
|
||||
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
|
||||
import { DashboardGridItem } from '../scene/DashboardGridItem';
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
|
||||
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
|
||||
import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior';
|
||||
import { NEW_LINK } from '../settings/links/utils';
|
||||
import { activateFullSceneTree, buildPanelRepeaterScene } from '../utils/test-utils';
|
||||
@ -44,11 +44,7 @@ import dashboard_to_load1 from './testfiles/dashboard_to_load1.json';
|
||||
import repeatingRowsAndPanelsDashboardJson from './testfiles/repeating_rows_and_panels.json';
|
||||
import snapshotableDashboardJson from './testfiles/snapshotable_dashboard.json';
|
||||
import snapshotableWithRowsDashboardJson from './testfiles/snapshotable_with_rows.json';
|
||||
import {
|
||||
buildGridItemForLibPanel,
|
||||
buildGridItemForPanel,
|
||||
transformSaveModelToScene,
|
||||
} from './transformSaveModelToScene';
|
||||
import { buildGridItemForPanel, transformSaveModelToScene } from './transformSaveModelToScene';
|
||||
import {
|
||||
gridItemToPanel,
|
||||
gridRowToSaveModel,
|
||||
@ -348,33 +344,33 @@ describe('transformSceneToSaveModel', () => {
|
||||
|
||||
describe('Library panels', () => {
|
||||
it('given a library panel', () => {
|
||||
// Not using buildGridItemFromPanelSchema since it strips options/fieldConfig
|
||||
const libVizPanel = new LibraryVizPanel({
|
||||
name: 'Some lib panel panel',
|
||||
title: 'A panel',
|
||||
uid: 'lib-panel-uid',
|
||||
panelKey: 'lib-panel',
|
||||
panel: new VizPanel({
|
||||
key: 'panel-4',
|
||||
title: 'Panel blahh blah',
|
||||
fieldConfig: {
|
||||
defaults: {},
|
||||
overrides: [],
|
||||
const libVizPanel = new VizPanel({
|
||||
key: 'panel-4',
|
||||
title: 'Panel blahh blah',
|
||||
$behaviors: [
|
||||
new LibraryPanelBehavior({
|
||||
name: 'Some lib panel panel',
|
||||
title: 'A panel',
|
||||
uid: 'lib-panel-uid',
|
||||
}),
|
||||
],
|
||||
fieldConfig: {
|
||||
defaults: {},
|
||||
overrides: [],
|
||||
},
|
||||
options: {
|
||||
legend: {
|
||||
calcs: [],
|
||||
displayMode: 'list',
|
||||
placement: 'bottom',
|
||||
showLegend: true,
|
||||
},
|
||||
options: {
|
||||
legend: {
|
||||
calcs: [],
|
||||
displayMode: 'list',
|
||||
placement: 'bottom',
|
||||
showLegend: true,
|
||||
},
|
||||
tooltip: {
|
||||
maxHeight: 600,
|
||||
mode: 'single',
|
||||
sort: 'none',
|
||||
},
|
||||
tooltip: {
|
||||
maxHeight: 600,
|
||||
mode: 'single',
|
||||
sort: 'none',
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const panel = new DashboardGridItem({
|
||||
@ -824,32 +820,33 @@ describe('transformSceneToSaveModel', () => {
|
||||
it('handles repeated library panels', () => {
|
||||
const { scene, repeater } = buildPanelRepeaterScene(
|
||||
{ variableQueryTime: 0, numberOfOptions: 2 },
|
||||
new LibraryVizPanel({
|
||||
name: 'Some lib panel panel',
|
||||
title: 'A panel',
|
||||
uid: 'lib-panel-uid',
|
||||
panelKey: 'lib-panel',
|
||||
panel: new VizPanel({
|
||||
key: 'panel-4',
|
||||
title: 'Panel blahh blah',
|
||||
fieldConfig: {
|
||||
defaults: {},
|
||||
overrides: [],
|
||||
new VizPanel({
|
||||
key: 'panel-4',
|
||||
title: 'Panel blahh blah',
|
||||
fieldConfig: {
|
||||
defaults: {},
|
||||
overrides: [],
|
||||
},
|
||||
options: {
|
||||
legend: {
|
||||
calcs: [],
|
||||
displayMode: 'list',
|
||||
placement: 'bottom',
|
||||
showLegend: true,
|
||||
},
|
||||
options: {
|
||||
legend: {
|
||||
calcs: [],
|
||||
displayMode: 'list',
|
||||
placement: 'bottom',
|
||||
showLegend: true,
|
||||
},
|
||||
tooltip: {
|
||||
maxHeight: 600,
|
||||
mode: 'single',
|
||||
sort: 'none',
|
||||
},
|
||||
tooltip: {
|
||||
maxHeight: 600,
|
||||
mode: 'single',
|
||||
sort: 'none',
|
||||
},
|
||||
}),
|
||||
},
|
||||
$behaviors: [
|
||||
new LibraryPanelBehavior({
|
||||
name: 'Some lib panel panel',
|
||||
title: 'A panel',
|
||||
uid: 'lib-panel-uid',
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
@ -1138,9 +1135,5 @@ describe('transformSceneToSaveModel', () => {
|
||||
});
|
||||
|
||||
export function buildGridItemFromPanelSchema(panel: Partial<Panel>) {
|
||||
if (panel.libraryPanel) {
|
||||
return buildGridItemForLibPanel(new PanelModel(panel))!;
|
||||
}
|
||||
|
||||
return buildGridItemForPanel(new PanelModel(panel));
|
||||
}
|
||||
|
@ -34,11 +34,10 @@ import { GrafanaQueryType } from 'app/plugins/datasource/grafana/types';
|
||||
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
|
||||
import { DashboardGridItem } from '../scene/DashboardGridItem';
|
||||
import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene';
|
||||
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
|
||||
import { PanelTimeRange } from '../scene/PanelTimeRange';
|
||||
import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior';
|
||||
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
|
||||
import { getPanelIdForVizPanel, getQueryRunnerFor } from '../utils/utils';
|
||||
import { getLibraryPanelBehavior, getPanelIdForVizPanel, getQueryRunnerFor, isLibraryPanel } from '../utils/utils';
|
||||
|
||||
import { GRAFANA_DATASOURCE_REF } from './const';
|
||||
import { dataLayersToAnnotations } from './dataLayersToAnnotations';
|
||||
@ -140,22 +139,6 @@ export function transformSceneToSaveModel(scene: DashboardScene, isSnapshot = fa
|
||||
return sortedDeepCloneWithoutNulls(dashboard);
|
||||
}
|
||||
|
||||
export function libraryVizPanelToPanel(libPanel: LibraryVizPanel, gridPos: GridPos): Panel {
|
||||
if (!libPanel.state.panel) {
|
||||
throw new Error('Library panel has no panel');
|
||||
}
|
||||
|
||||
return {
|
||||
id: getPanelIdForVizPanel(libPanel.state.panel),
|
||||
title: libPanel.state.title,
|
||||
gridPos: gridPos,
|
||||
libraryPanel: {
|
||||
name: libPanel.state.name,
|
||||
uid: libPanel.state.uid,
|
||||
},
|
||||
} as Panel;
|
||||
}
|
||||
|
||||
export function gridItemToPanel(
|
||||
gridItem: DashboardGridItem,
|
||||
sceneState?: DashboardSceneState,
|
||||
@ -167,16 +150,6 @@ export function gridItemToPanel(
|
||||
w = 0,
|
||||
h = 0;
|
||||
|
||||
// Handle library panels, early exit
|
||||
if (gridItem.state.body instanceof LibraryVizPanel) {
|
||||
x = gridItem.state.x ?? 0;
|
||||
y = gridItem.state.y ?? 0;
|
||||
w = gridItem.state.width ?? 0;
|
||||
h = gridItem.state.itemHeight ?? gridItem.state.height ?? 0;
|
||||
|
||||
return libraryVizPanelToPanel(gridItem.state.body, { x, y, w, h });
|
||||
}
|
||||
|
||||
let gridItem_ = gridItem;
|
||||
|
||||
// If we're saving while the panel editor is open, we need to persist those changes in the panel model
|
||||
@ -186,7 +159,7 @@ export function gridItemToPanel(
|
||||
sceneState.editPanel.state.vizManager.state.sourcePanel.resolve() === gridItem.state.body
|
||||
) {
|
||||
const gridItemClone = gridItem.clone();
|
||||
if (gridItemClone.state.body instanceof VizPanel) {
|
||||
if (gridItemClone.state.body instanceof VizPanel && !isLibraryPanel(gridItemClone.state.body)) {
|
||||
sceneState.editPanel.state.vizManager.commitChangesTo(gridItemClone.state.body);
|
||||
gridItem_ = gridItemClone;
|
||||
}
|
||||
@ -217,18 +190,36 @@ export function vizPanelToPanel(
|
||||
isSnapshot = false,
|
||||
gridItem?: SceneGridItemLike
|
||||
) {
|
||||
const panel: Panel = {
|
||||
id: getPanelIdForVizPanel(vizPanel),
|
||||
type: vizPanel.state.pluginId,
|
||||
title: vizPanel.state.title,
|
||||
description: vizPanel.state.description ?? undefined,
|
||||
gridPos,
|
||||
fieldConfig: (vizPanel.state.fieldConfig as FieldConfigSource) ?? { defaults: {}, overrides: [] },
|
||||
transformations: [],
|
||||
transparent: vizPanel.state.displayMode === 'transparent',
|
||||
pluginVersion: vizPanel.state.pluginVersion,
|
||||
...vizPanelDataToPanel(vizPanel, isSnapshot),
|
||||
};
|
||||
let panel: Panel;
|
||||
|
||||
if (isLibraryPanel(vizPanel)) {
|
||||
const libPanel = getLibraryPanelBehavior(vizPanel);
|
||||
|
||||
panel = {
|
||||
id: getPanelIdForVizPanel(vizPanel),
|
||||
title: libPanel!.state.title,
|
||||
gridPos: gridPos,
|
||||
libraryPanel: {
|
||||
name: libPanel!.state.name,
|
||||
uid: libPanel!.state.uid,
|
||||
},
|
||||
} as Panel;
|
||||
|
||||
return panel;
|
||||
} else {
|
||||
panel = {
|
||||
id: getPanelIdForVizPanel(vizPanel),
|
||||
type: vizPanel.state.pluginId,
|
||||
title: vizPanel.state.title,
|
||||
description: vizPanel.state.description ?? undefined,
|
||||
gridPos,
|
||||
fieldConfig: (vizPanel.state.fieldConfig as FieldConfigSource) ?? { defaults: {}, overrides: [] },
|
||||
transformations: [],
|
||||
transparent: vizPanel.state.displayMode === 'transparent',
|
||||
pluginVersion: vizPanel.state.pluginVersion,
|
||||
...vizPanelDataToPanel(vizPanel, isSnapshot),
|
||||
};
|
||||
}
|
||||
|
||||
if (vizPanel.state.options) {
|
||||
const { angularOptions, ...rest } = vizPanel.state.options as any;
|
||||
@ -342,9 +333,10 @@ export function panelRepeaterToPanels(
|
||||
if (!isSnapshot) {
|
||||
return [gridItemToPanel(repeater, sceneState)];
|
||||
} else {
|
||||
if (repeater.state.body instanceof LibraryVizPanel) {
|
||||
// return early if the repeated panel is a library panel
|
||||
if (repeater.state.body instanceof VizPanel && isLibraryPanel(repeater.state.body)) {
|
||||
const { x = 0, y = 0, width: w = 0, height: h = 0 } = repeater.state;
|
||||
return [libraryVizPanelToPanel(repeater.state.body, { x, y, w, h })];
|
||||
return [vizPanelToPanel(repeater.state.body, { x, y, w, h }, isSnapshot)];
|
||||
}
|
||||
|
||||
if (repeater.state.repeatedPanels) {
|
||||
|
@ -8,9 +8,8 @@ import { t } from 'app/core/internationalization';
|
||||
import { isPublicDashboardsEnabled } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils';
|
||||
|
||||
import { getTrackingSource } from '../../dashboard/components/ShareModal/utils';
|
||||
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
|
||||
import { DashboardInteractions } from '../utils/interactions';
|
||||
import { getDashboardSceneFor } from '../utils/utils';
|
||||
import { getDashboardSceneFor, isLibraryPanel } from '../utils/utils';
|
||||
|
||||
import { ShareExportTab } from './ShareExportTab';
|
||||
import { ShareLibraryPanelTab } from './ShareLibraryPanelTab';
|
||||
@ -66,9 +65,8 @@ export class ShareModal extends SceneObjectBase<ShareModalState> implements Moda
|
||||
if (panelRef) {
|
||||
tabs.push(new SharePanelEmbedTab({ panelRef }));
|
||||
const panel = panelRef.resolve();
|
||||
const isLibraryPanel = panel.parent instanceof LibraryVizPanel;
|
||||
if (panel instanceof VizPanel) {
|
||||
if (!isLibraryPanel) {
|
||||
if (!isLibraryPanel(panel)) {
|
||||
tabs.push(new ShareLibraryPanelTab({ panelRef, modalRef }));
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
|
||||
import { DashboardGridItem } from '../../scene/DashboardGridItem';
|
||||
import { DashboardScene } from '../../scene/DashboardScene';
|
||||
import { LibraryVizPanel } from '../../scene/LibraryVizPanel';
|
||||
|
||||
export const getUnsupportedDashboardDatasources = async (types: string[]): Promise<string[]> => {
|
||||
let unsupportedDS = new Set<string>();
|
||||
@ -64,12 +63,10 @@ function rowTypes(gridRow: SceneGridRow) {
|
||||
}
|
||||
|
||||
function panelDatasourceTypes(gridItem: SceneGridItemLike) {
|
||||
let vizPanel: VizPanel | LibraryVizPanel | undefined;
|
||||
let vizPanel: VizPanel | undefined;
|
||||
|
||||
if (gridItem instanceof DashboardGridItem) {
|
||||
if (gridItem.state.body instanceof LibraryVizPanel) {
|
||||
vizPanel = gridItem.state.body.state.panel;
|
||||
} else if (gridItem.state.body instanceof VizPanel) {
|
||||
if (gridItem.state.body instanceof VizPanel) {
|
||||
vizPanel = gridItem.state.body;
|
||||
} else {
|
||||
throw new Error('DashboardGridItem body expected to be VizPanel');
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { VizPanel } from '@grafana/scenes';
|
||||
|
||||
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
|
||||
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
|
||||
|
||||
import { PanelModelCompatibilityWrapper } from './PanelModelCompatibilityWrapper';
|
||||
|
||||
@ -12,15 +12,19 @@ describe('PanelModelCompatibilityWrapper', () => {
|
||||
});
|
||||
|
||||
it('Can get legacy id for lib panel', () => {
|
||||
const libPanel = new LibraryVizPanel({
|
||||
const panel = new VizPanel({ pluginId: 'test', title: 'test', description: 'test', key: 'panel-24' });
|
||||
|
||||
const libPanel = new LibraryPanelBehavior({
|
||||
uid: 'a',
|
||||
name: 'aa',
|
||||
title: 'a',
|
||||
panelKey: 'panel-24',
|
||||
panel: new VizPanel({ pluginId: 'test', title: 'test', description: 'test', key: 'panel-24' }),
|
||||
});
|
||||
|
||||
const panelModel = new PanelModelCompatibilityWrapper(libPanel.state.panel!);
|
||||
panel.setState({
|
||||
$behaviors: [libPanel],
|
||||
});
|
||||
|
||||
const panelModel = new PanelModelCompatibilityWrapper(panel);
|
||||
expect(panelModel.id).toBe(24);
|
||||
});
|
||||
});
|
||||
|
@ -6,7 +6,7 @@ import { DashboardControls } from '../scene/DashboardControls';
|
||||
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
|
||||
import { DashboardGridItem } from '../scene/DashboardGridItem';
|
||||
import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene';
|
||||
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
|
||||
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
|
||||
import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks';
|
||||
|
||||
import { dashboardSceneGraph, getNextPanelId } from './dashboardSceneGraph';
|
||||
@ -129,11 +129,16 @@ describe('dashboardSceneGraph', () => {
|
||||
}),
|
||||
}),
|
||||
new DashboardGridItem({
|
||||
body: new LibraryVizPanel({
|
||||
uid: 'uid',
|
||||
name: 'LibPanel',
|
||||
title: 'Library Panel',
|
||||
panelKey: 'panel-2',
|
||||
body: new VizPanel({
|
||||
title: 'Library Panel 1',
|
||||
key: 'panel-2',
|
||||
$behaviors: [
|
||||
new LibraryPanelBehavior({
|
||||
uid: 'uid',
|
||||
name: 'LibPanel',
|
||||
title: 'Library Panel 1',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
new DashboardGridItem({
|
||||
@ -166,11 +171,16 @@ describe('dashboardSceneGraph', () => {
|
||||
}),
|
||||
}),
|
||||
new DashboardGridItem({
|
||||
body: new LibraryVizPanel({
|
||||
uid: 'uid',
|
||||
name: 'LibPanel',
|
||||
title: 'Library Panel',
|
||||
panelKey: 'panel-3',
|
||||
body: new VizPanel({
|
||||
title: 'Library Panel 2',
|
||||
key: 'panel-3',
|
||||
$behaviors: [
|
||||
new LibraryPanelBehavior({
|
||||
uid: 'uid',
|
||||
name: 'LibPanel',
|
||||
title: 'Library Panel 2',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
],
|
||||
|
@ -3,10 +3,9 @@ import { VizPanel, SceneGridRow, sceneGraph, SceneGridLayout, behaviors } from '
|
||||
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
|
||||
import { DashboardGridItem } from '../scene/DashboardGridItem';
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
|
||||
import { VizPanelLinks } from '../scene/PanelLinks';
|
||||
|
||||
import { getPanelIdForLibraryVizPanel, getPanelIdForVizPanel } from './utils';
|
||||
import { getPanelIdForVizPanel } from './utils';
|
||||
|
||||
function getTimePicker(scene: DashboardScene) {
|
||||
return scene.state.controls?.state.timePicker;
|
||||
@ -87,10 +86,7 @@ export function getNextPanelId(dashboard: DashboardScene): number {
|
||||
const vizPanel = child.state.body;
|
||||
|
||||
if (vizPanel) {
|
||||
const panelId =
|
||||
vizPanel instanceof LibraryVizPanel
|
||||
? getPanelIdForLibraryVizPanel(vizPanel)
|
||||
: getPanelIdForVizPanel(vizPanel);
|
||||
const panelId = getPanelIdForVizPanel(vizPanel);
|
||||
|
||||
if (panelId > max) {
|
||||
max = panelId;
|
||||
@ -111,10 +107,7 @@ export function getNextPanelId(dashboard: DashboardScene): number {
|
||||
const vizPanel = rowChild.state.body;
|
||||
|
||||
if (vizPanel) {
|
||||
const panelId =
|
||||
vizPanel instanceof LibraryVizPanel
|
||||
? getPanelIdForLibraryVizPanel(vizPanel)
|
||||
: getPanelIdForVizPanel(vizPanel);
|
||||
const panelId = getPanelIdForVizPanel(vizPanel);
|
||||
|
||||
if (panelId > max) {
|
||||
max = panelId;
|
||||
@ -128,19 +121,6 @@ export function getNextPanelId(dashboard: DashboardScene): number {
|
||||
return max + 1;
|
||||
}
|
||||
|
||||
// Returns the LibraryVizPanel that corresponds to the given VizPanel if it exists
|
||||
export const getLibraryVizPanelFromVizPanel = (vizPanel: VizPanel): LibraryVizPanel | null => {
|
||||
if (vizPanel.parent instanceof LibraryVizPanel) {
|
||||
return vizPanel.parent;
|
||||
}
|
||||
|
||||
if (vizPanel.parent instanceof DashboardGridItem && vizPanel.parent.state.body instanceof LibraryVizPanel) {
|
||||
return vizPanel.parent.state.body;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const dashboardSceneGraph = {
|
||||
getTimePicker,
|
||||
getRefreshPicker,
|
||||
|
@ -16,7 +16,6 @@ import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from 'app/features/variables/co
|
||||
import { DashboardDTO } from 'app/types';
|
||||
|
||||
import { DashboardGridItem, RepeatDirection } from '../scene/DashboardGridItem';
|
||||
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
|
||||
import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks';
|
||||
import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior';
|
||||
|
||||
@ -107,7 +106,7 @@ interface SceneOptions {
|
||||
variableRefresh?: VariableRefresh;
|
||||
}
|
||||
|
||||
export function buildPanelRepeaterScene(options: SceneOptions, source?: VizPanel | LibraryVizPanel) {
|
||||
export function buildPanelRepeaterScene(options: SceneOptions, source?: VizPanel) {
|
||||
const defaults = { usePanelRepeater: true, ...options };
|
||||
|
||||
const withRepeat = new DashboardGridItem({
|
||||
|
@ -15,7 +15,7 @@ import { initialIntervalVariableModelState } from 'app/features/variables/interv
|
||||
|
||||
import { DashboardDatasourceBehaviour } from '../scene/DashboardDatasourceBehaviour';
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
|
||||
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
|
||||
import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks';
|
||||
import { panelMenuBehavior } from '../scene/PanelMenuBehavior';
|
||||
import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior';
|
||||
@ -34,10 +34,6 @@ export function getPanelIdForVizPanel(panel: SceneObject): number {
|
||||
return parseInt(panel.state.key!.replace('panel-', ''), 10);
|
||||
}
|
||||
|
||||
export function getPanelIdForLibraryVizPanel(panel: LibraryVizPanel): number {
|
||||
return parseInt(panel.state.panelKey!.replace('panel-', ''), 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* This will also try lookup based on panelId
|
||||
*/
|
||||
@ -256,11 +252,18 @@ export function getDefaultRow(dashboard: DashboardScene): SceneGridRow {
|
||||
});
|
||||
}
|
||||
|
||||
export function getLibraryPanel(vizPanel: VizPanel): LibraryVizPanel | undefined {
|
||||
if (vizPanel.parent instanceof LibraryVizPanel) {
|
||||
return vizPanel.parent;
|
||||
export function isLibraryPanel(vizPanel: VizPanel): boolean {
|
||||
return getLibraryPanelBehavior(vizPanel) !== undefined;
|
||||
}
|
||||
|
||||
export function getLibraryPanelBehavior(vizPanel: VizPanel): LibraryPanelBehavior | undefined {
|
||||
const behavior = vizPanel.state.$behaviors?.find((behaviour) => behaviour instanceof LibraryPanelBehavior);
|
||||
|
||||
if (behavior) {
|
||||
return behavior;
|
||||
}
|
||||
return;
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -17,7 +17,7 @@ import {
|
||||
import { VizPanel } from '@grafana/scenes';
|
||||
import { Input } from '@grafana/ui';
|
||||
import { LibraryVizPanelInfo } from 'app/features/dashboard-scene/panel-edit/LibraryVizPanelInfo';
|
||||
import { LibraryVizPanel } from 'app/features/dashboard-scene/scene/LibraryVizPanel';
|
||||
import { LibraryPanelBehavior } from 'app/features/dashboard-scene/scene/LibraryPanelBehavior';
|
||||
import { getDataLinksVariableSuggestions } from 'app/features/panel/panellinks/link_srv';
|
||||
|
||||
import { OptionsPaneCategoryDescriptor } from './OptionsPaneCategoryDescriptor';
|
||||
@ -150,7 +150,7 @@ export function getVisualizationOptions(props: OptionPaneRenderProps): OptionsPa
|
||||
return Object.values(categoryIndex);
|
||||
}
|
||||
|
||||
export function getLibraryVizPanelOptionsCategory(libraryPanel: LibraryVizPanel): OptionsPaneCategoryDescriptor {
|
||||
export function getLibraryVizPanelOptionsCategory(libraryPanel: LibraryPanelBehavior): OptionsPaneCategoryDescriptor {
|
||||
const descriptor = new OptionsPaneCategoryDescriptor({
|
||||
title: 'Library panel options',
|
||||
id: 'Library panel options',
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
|
||||
import { defaultDashboard } from '@grafana/schema';
|
||||
import { VizPanel } from '@grafana/scenes';
|
||||
import { LibraryPanel, defaultDashboard } from '@grafana/schema';
|
||||
import { DashboardModel } from 'app/features/dashboard/state';
|
||||
import { VizPanelManager } from 'app/features/dashboard-scene/panel-edit/VizPanelManager';
|
||||
import { DashboardGridItem } from 'app/features/dashboard-scene/scene/DashboardGridItem';
|
||||
import { LibraryVizPanel } from 'app/features/dashboard-scene/scene/LibraryVizPanel';
|
||||
import { vizPanelToPanel } from 'app/features/dashboard-scene/serialization/transformSceneToSaveModel';
|
||||
import { getLibraryPanelBehavior } from 'app/features/dashboard-scene/utils/utils';
|
||||
|
||||
import { getBackendSrv } from '../../../core/services/backend_srv';
|
||||
import { DashboardSearchItem } from '../../search/types';
|
||||
@ -137,22 +139,30 @@ export async function getConnectedDashboards(uid: string): Promise<DashboardSear
|
||||
return searchHits;
|
||||
}
|
||||
|
||||
export function libraryVizPanelToSaveModel(libraryPanel: LibraryVizPanel) {
|
||||
const { panel, uid, name, _loadedPanel } = libraryPanel.state;
|
||||
export function libraryVizPanelToSaveModel(vizPanel: VizPanel) {
|
||||
const libraryPanelBehavior = getLibraryPanelBehavior(vizPanel);
|
||||
|
||||
const gridItem = libraryPanel.parent;
|
||||
const { uid, name, _loadedPanel } = libraryPanelBehavior!.state;
|
||||
|
||||
if (!gridItem || !(gridItem instanceof DashboardGridItem)) {
|
||||
throw new Error('LibraryVizPanel must be a child of DashboardGridItem');
|
||||
let gridItem = vizPanel.parent;
|
||||
|
||||
if (gridItem instanceof VizPanelManager) {
|
||||
gridItem = gridItem.state.sourcePanel.resolve().parent;
|
||||
}
|
||||
|
||||
if (!gridItem || !(gridItem instanceof DashboardGridItem)) {
|
||||
throw new Error('Trying to save a library panel that does not have a DashboardGridItem parent');
|
||||
}
|
||||
|
||||
// we need all the panel properties to save the library panel,
|
||||
// so we clone it and remove the behaviour to get what we need
|
||||
const saveModel = {
|
||||
..._loadedPanel,
|
||||
uid,
|
||||
folderUID: _loadedPanel?.folderUid,
|
||||
name,
|
||||
version: _loadedPanel?.version || 0,
|
||||
type: vizPanel.state.pluginId,
|
||||
model: vizPanelToPanel(
|
||||
panel!,
|
||||
vizPanel.clone({ $behaviors: undefined }),
|
||||
{
|
||||
x: gridItem.state.x ?? 0,
|
||||
y: gridItem.state.y ?? 0,
|
||||
@ -163,15 +173,16 @@ export function libraryVizPanelToSaveModel(libraryPanel: LibraryVizPanel) {
|
||||
gridItem
|
||||
),
|
||||
kind: LibraryElementKind.Panel,
|
||||
version: _loadedPanel?.version || 0,
|
||||
};
|
||||
return saveModel;
|
||||
}
|
||||
|
||||
export async function updateLibraryVizPanel(libraryPanel: LibraryVizPanel): Promise<LibraryElementDTO> {
|
||||
const { uid, folderUID, name, model, version, kind } = libraryVizPanelToSaveModel(libraryPanel);
|
||||
export async function updateLibraryVizPanel(vizPanel: VizPanel): Promise<LibraryPanel> {
|
||||
const { uid, folderUid, name, model, version, kind } = libraryVizPanelToSaveModel(vizPanel);
|
||||
|
||||
const { result } = await getBackendSrv().patch(`/api/library-elements/${uid}`, {
|
||||
folderUID,
|
||||
folderUid,
|
||||
name,
|
||||
model,
|
||||
version,
|
||||
@ -179,3 +190,15 @@ export async function updateLibraryVizPanel(libraryPanel: LibraryVizPanel): Prom
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function saveLibPanel(panel: VizPanel) {
|
||||
const updatedLibPanel = await updateLibraryVizPanel(panel);
|
||||
|
||||
const libPanelBehavior = getLibraryPanelBehavior(panel);
|
||||
|
||||
if (!libPanelBehavior) {
|
||||
throw new Error('Could not find library panel behavior for panel');
|
||||
}
|
||||
|
||||
libPanelBehavior.setPanelFromLibPanel(updatedLibPanel);
|
||||
}
|
||||
|
@ -1144,6 +1144,7 @@
|
||||
"empty-state": {
|
||||
"message": "No library panels found"
|
||||
},
|
||||
"loading-panel-text": "Loading library panel",
|
||||
"modal": {
|
||||
"body_one": "This panel is being used in {{count}} dashboard. Please choose which dashboard to view the panel in:",
|
||||
"body_other": "This panel is being used in {{count}} dashboard. Please choose which dashboard to view the panel in:",
|
||||
|
@ -1144,6 +1144,7 @@
|
||||
"empty-state": {
|
||||
"message": "Ńő ľįþřäřy päʼnęľş ƒőūʼnđ"
|
||||
},
|
||||
"loading-panel-text": "Ŀőäđįʼnģ ľįþřäřy päʼnęľ",
|
||||
"modal": {
|
||||
"body_one": "Ŧĥįş päʼnęľ įş þęįʼnģ ūşęđ įʼn {{count}} đäşĥþőäřđ. Pľęäşę čĥőőşę ŵĥįčĥ đäşĥþőäřđ ŧő vįęŵ ŧĥę päʼnęľ įʼn:",
|
||||
"body_other": "Ŧĥįş päʼnęľ įş þęįʼnģ ūşęđ įʼn {{count}} đäşĥþőäřđ. Pľęäşę čĥőőşę ŵĥįčĥ đäşĥþőäřđ ŧő vįęŵ ŧĥę päʼnęľ įʼn:",
|
||||
|
Loading…
Reference in New Issue
Block a user