From e86784200a77964e2bc88d11d3348af28a7b0ece Mon Sep 17 00:00:00 2001 From: Victor Marin <36818606+mdvictor@users.noreply.github.com> Date: Mon, 3 Jun 2024 11:02:41 +0300 Subject: [PATCH] Create lib panel and replace it in scene (#88504) * Create lib panel - also replace it in scene * refactor * refactor * refactor --- .../scene/DashboardScene.test.tsx | 72 ++++++++++++++++++- .../dashboard-scene/scene/DashboardScene.tsx | 49 ++++++++++++- .../sharing/ShareLibraryPanelTab.tsx | 8 ++- .../ShareModal/ShareLibraryPanel.tsx | 9 ++- .../dashboard/components/ShareModal/types.ts | 2 + .../AddLibraryPanelModal.tsx | 15 ++-- 6 files changed, 144 insertions(+), 11 deletions(-) diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx index 041c692036f..2667327ca69 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx @@ -12,7 +12,7 @@ import { behaviors, SceneDataTransformer, } from '@grafana/scenes'; -import { Dashboard, DashboardCursorSync } from '@grafana/schema'; +import { Dashboard, DashboardCursorSync, LibraryPanel } from '@grafana/schema'; import appEvents from 'app/core/app_events'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { VariablesChanged } from 'app/features/variables/types'; @@ -888,6 +888,76 @@ describe('DashboardScene', () => { expect(body.state.children.length).toBe(1); expect(gridItem.state.body).toBeInstanceOf(VizPanel); }); + + it('Should create a library panel', () => { + const gridItem = new DashboardGridItem({ + key: 'griditem-1', + body: new VizPanel({ + title: 'Panel A', + key: 'panel-1', + pluginId: 'table', + }), + }); + + const scene = buildTestScene({ + body: new SceneGridLayout({ + children: [gridItem], + }), + }); + + const libPanel = { + uid: 'uid', + name: 'name', + }; + + scene.createLibraryPanel(gridItem, libPanel as LibraryPanel); + + const layout = scene.state.body as SceneGridLayout; + const newGridItem = layout.state.children[0] as DashboardGridItem; + + 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'); + }); + + it('Should create a library panel under a row', () => { + const gridItem = new DashboardGridItem({ + key: 'griditem-1', + body: new VizPanel({ + title: 'Panel A', + key: 'panel-1', + pluginId: 'table', + }), + }); + + const scene = buildTestScene({ + body: new SceneGridLayout({ + children: [ + new SceneGridRow({ + key: 'row-1', + children: [gridItem], + }), + ], + }), + }); + + const libPanel = { + uid: 'uid', + name: 'name', + }; + + scene.createLibraryPanel(gridItem, libPanel as LibraryPanel); + + const layout = scene.state.body as SceneGridLayout; + const newGridItem = (layout.state.children[0] as SceneGridRow).state.children[0] as DashboardGridItem; + + 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'); + }); }); }); diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index dea0fb29b4a..4f4e319ed3b 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -16,7 +16,7 @@ import { SceneVariableDependencyConfigLike, VizPanel, } from '@grafana/scenes'; -import { Dashboard, DashboardLink } from '@grafana/schema'; +import { Dashboard, DashboardLink, LibraryPanel } from '@grafana/schema'; import appEvents from 'app/core/app_events'; import { LS_PANEL_COPY_KEY } from 'app/core/constants'; import { getNavModel } from 'app/core/selectors/navModel'; @@ -510,6 +510,53 @@ export class DashboardScene extends SceneObjectBase { }); } + public createLibraryPanel(gridItemToReplace: DashboardGridItem, libPanel: LibraryPanel) { + const layout = this.state.body; + + if (!(layout instanceof SceneGridLayout)) { + throw new Error('Trying to add a panel in a layout that is not SceneGridLayout'); + } + + const panelKey = gridItemToReplace?.state.body.state.key; + + const body = new LibraryVizPanel({ + title: libPanel.model?.title ?? 'Panel', + uid: libPanel.uid, + name: libPanel.name, + panelKey: panelKey ?? getVizPanelKeyForPanelId(dashboardSceneGraph.getNextPanelId(this)), + }); + + const newGridItem = gridItemToReplace.clone({ body }); + + if (!(newGridItem instanceof DashboardGridItem)) { + throw new Error('Could not build library viz panel griditem'); + } + + const key = gridItemToReplace?.state.key; + + if (gridItemToReplace.parent instanceof SceneGridRow) { + const children = gridItemToReplace.parent.state.children.map((rowChild) => { + if (rowChild.state.key === key) { + return newGridItem; + } + return rowChild; + }); + + gridItemToReplace.parent.setState({ children }); + layout.forceRender(); + } else { + // Find the grid item in the layout and replace it + const children = layout.state.children.map((child) => { + if (child.state.key === key) { + return newGridItem; + } + return child; + }); + + layout.setState({ children }); + } + } + public duplicatePanel(vizPanel: VizPanel) { if (!vizPanel.parent) { return; diff --git a/public/app/features/dashboard-scene/sharing/ShareLibraryPanelTab.tsx b/public/app/features/dashboard-scene/sharing/ShareLibraryPanelTab.tsx index 601d2e3b31c..e78f2501e3e 100644 --- a/public/app/features/dashboard-scene/sharing/ShareLibraryPanelTab.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareLibraryPanelTab.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { SceneComponentProps, SceneObjectBase, SceneObjectRef, VizPanel } from '@grafana/scenes'; +import { LibraryPanel } from '@grafana/schema/dist/esm/index.gen'; import { t } from 'app/core/internationalization'; import { ShareLibraryPanel } from 'app/features/dashboard/components/ShareModal/ShareLibraryPanel'; import { shareDashboardType } from 'app/features/dashboard/components/ShareModal/utils'; @@ -31,11 +32,11 @@ function ShareLibraryPanelTabRenderer({ model }: SceneComponentProps { modalRef?.resolve().onDismiss(); }} + onCreateLibraryPanel={(libPanel: LibraryPanel) => dashboardScene.createLibraryPanel(parent, libPanel)} /> ); } diff --git a/public/app/features/dashboard/components/ShareModal/ShareLibraryPanel.tsx b/public/app/features/dashboard/components/ShareModal/ShareLibraryPanel.tsx index 9b76f04af65..a67d7d050cb 100644 --- a/public/app/features/dashboard/components/ShareModal/ShareLibraryPanel.tsx +++ b/public/app/features/dashboard/components/ShareModal/ShareLibraryPanel.tsx @@ -11,7 +11,7 @@ interface Props extends ShareModalTabProps { initialFolderUid?: string; } -export const ShareLibraryPanel = ({ panel, initialFolderUid, onDismiss }: Props) => { +export const ShareLibraryPanel = ({ panel, initialFolderUid, onCreateLibraryPanel, onDismiss }: Props) => { useEffect(() => { reportInteraction('grafana_dashboards_library_panel_share_viewed', { shareResource: getTrackingSource(panel) }); }, [panel]); @@ -25,7 +25,12 @@ export const ShareLibraryPanel = ({ panel, initialFolderUid, onDismiss }: Props)

Create library panel.

- + ); }; diff --git a/public/app/features/dashboard/components/ShareModal/types.ts b/public/app/features/dashboard/components/ShareModal/types.ts index 558bd0fa8a9..14cbe0fab24 100644 --- a/public/app/features/dashboard/components/ShareModal/types.ts +++ b/public/app/features/dashboard/components/ShareModal/types.ts @@ -1,12 +1,14 @@ import React from 'react'; import { NavModelItem } from '@grafana/data'; +import { LibraryPanel } from '@grafana/schema/dist/esm/index.gen'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; export interface ShareModalTabProps { dashboard: DashboardModel; panel?: PanelModel; onDismiss?(): void; + onCreateLibraryPanel?(libPanel: LibraryPanel): void; } export interface ShareModalTabModel { diff --git a/public/app/features/library-panels/components/AddLibraryPanelModal/AddLibraryPanelModal.tsx b/public/app/features/library-panels/components/AddLibraryPanelModal/AddLibraryPanelModal.tsx index 697b9197b6a..d9dfdc14d9a 100644 --- a/public/app/features/library-panels/components/AddLibraryPanelModal/AddLibraryPanelModal.tsx +++ b/public/app/features/library-panels/components/AddLibraryPanelModal/AddLibraryPanelModal.tsx @@ -2,22 +2,28 @@ import React, { useCallback, useEffect, useState } from 'react'; import { useAsync, useDebounce } from 'react-use'; import { FetchError, isFetchError } from '@grafana/runtime'; +import { LibraryPanel } from '@grafana/schema/dist/esm/index.gen'; import { Button, Field, Input, Modal } from '@grafana/ui'; import { OldFolderPicker } from 'app/core/components/Select/OldFolderPicker'; import { t, Trans } from 'app/core/internationalization'; import { PanelModel } from '../../../dashboard/state'; import { getLibraryPanelByName } from '../../state/api'; -import { LibraryElementDTO } from '../../types'; import { usePanelSave } from '../../utils/usePanelSave'; interface AddLibraryPanelContentsProps { onDismiss?: () => void; panel: PanelModel; initialFolderUid?: string; + onCreateLibraryPanel?: (libPanel: LibraryPanel) => void; } -export const AddLibraryPanelContents = ({ panel, initialFolderUid, onDismiss }: AddLibraryPanelContentsProps) => { +export const AddLibraryPanelContents = ({ + panel, + initialFolderUid, + onCreateLibraryPanel, + onDismiss, +}: AddLibraryPanelContentsProps) => { const [folderUid, setFolderUid] = useState(initialFolderUid); const [panelName, setPanelName] = useState(panel.title); const [debouncedPanelName, setDebouncedPanelName] = useState(panel.title); @@ -30,14 +36,15 @@ export const AddLibraryPanelContents = ({ panel, initialFolderUid, onDismiss }: const onCreate = useCallback(() => { panel.libraryPanel = { uid: '', name: panelName }; - saveLibraryPanel(panel, folderUid!).then((res: LibraryElementDTO | FetchError) => { + saveLibraryPanel(panel, folderUid!).then((res: LibraryPanel | FetchError) => { if (!isFetchError(res)) { onDismiss?.(); + onCreateLibraryPanel?.(res); } else { panel.libraryPanel = undefined; } }); - }, [panel, panelName, folderUid, onDismiss, saveLibraryPanel]); + }, [panel, panelName, saveLibraryPanel, folderUid, onDismiss, onCreateLibraryPanel]); const isValidName = useAsync(async () => { try {