diff --git a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx index 199a2e06586..838b7d948a2 100644 --- a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx +++ b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx @@ -38,7 +38,7 @@ describe('panelMenuBehavior', () => { await new Promise((r) => setTimeout(r, 1)); - expect(menu.state.items?.length).toBe(5); + expect(menu.state.items?.length).toBe(6); // verify view panel url keeps url params and adds viewPanel= expect(menu.state.items?.[0].href).toBe('/scenes/dashboard/dash-1?from=now-5m&to=now&viewPanel=panel-12'); // verify edit url keeps url time range diff --git a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx index 7a25aeca24e..7029a5faf47 100644 --- a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx +++ b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx @@ -12,6 +12,7 @@ import { getDashboardUrl, getInspectUrl, getViewPanelUrl, tryGetExploreUrlForPan import { getPanelIdForVizPanel } from '../utils/utils'; import { DashboardScene } from './DashboardScene'; +import { LibraryVizPanel } from './LibraryVizPanel'; import { VizPanelLinks } from './PanelLinks'; /** @@ -24,6 +25,7 @@ export function panelMenuBehavior(menu: VizPanelMenu) { const panel = menu.parent as VizPanel; const location = locationService.getLocation(); const items: PanelMenuItem[] = []; + const moreSubMenu: PanelMenuItem[] = []; const panelId = getPanelIdForVizPanel(panel); const dashboard = panel.getRoot(); @@ -63,6 +65,25 @@ export function panelMenuBehavior(menu: VizPanelMenu) { shortcut: 'p s', }); + if (panel instanceof LibraryVizPanel) { + // TODO: Implement unlinking library panel + } else { + moreSubMenu.push({ + text: t('panel.header-menu.create-library-panel', `Create library panel`), + iconClassName: 'share-alt', + onClick: () => { + reportInteraction('dashboards_panelheader_menu', { item: 'createLibraryPanel' }); + dashboard.showModal( + new ShareModal({ + panelRef: panel.getRef(), + dashboardRef: dashboard.getRef(), + activeTab: 'Library panel', + }) + ); + }, + }); + } + if (config.featureToggles.datatrails) { addDataTrailPanelAction(dashboard, panel, items); } @@ -87,6 +108,18 @@ export function panelMenuBehavior(menu: VizPanelMenu) { href: getInspectUrl(panel), }); + if (moreSubMenu.length) { + items.push({ + type: 'submenu', + text: t('panel.header-menu.more', `More...`), + iconClassName: 'cube', + subMenu: moreSubMenu, + onClick: (e) => { + e.preventDefault(); + }, + }); + } + menu.setState({ items }); }; diff --git a/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx b/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx index 352f8438997..067cb6f787a 100644 --- a/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx @@ -25,6 +25,7 @@ interface ShareExportTabState extends SceneShareTabState { } export class ShareExportTab extends SceneObjectBase { + public tabId = 'Export'; static Component = ShareExportTabRenderer; private _exporter = new DashboardExporter(); diff --git a/public/app/features/dashboard-scene/sharing/ShareLibraryPanelTab.tsx b/public/app/features/dashboard-scene/sharing/ShareLibraryPanelTab.tsx new file mode 100644 index 00000000000..e4e839b854d --- /dev/null +++ b/public/app/features/dashboard-scene/sharing/ShareLibraryPanelTab.tsx @@ -0,0 +1,58 @@ +import React from 'react'; + +import { SceneComponentProps, SceneGridItem, SceneObjectBase, SceneObjectRef, VizPanel } from '@grafana/scenes'; +import { t } from 'app/core/internationalization'; +import { ShareLibraryPanel } from 'app/features/dashboard/components/ShareModal/ShareLibraryPanel'; +import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; + +import { DashboardScene } from '../scene/DashboardScene'; +import { PanelRepeaterGridItem } from '../scene/PanelRepeaterGridItem'; +import { gridItemToPanel, transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel'; + +import { SceneShareTabState } from './types'; + +export interface ShareLibraryPanelTabState extends SceneShareTabState { + panelRef?: SceneObjectRef; + dashboardRef: SceneObjectRef; +} + +export class ShareLibraryPanelTab extends SceneObjectBase { + public tabId = 'Library panel'; + static Component = ShareLibraryPanelTabRenderer; + + public getTabLabel() { + return t('share-modal.tab-title.library-panel', 'Library panel'); + } +} + +function ShareLibraryPanelTabRenderer({ model }: SceneComponentProps) { + const { panelRef, dashboardRef, modalRef } = model.useState(); + + if (!panelRef) { + return null; + } + + const vizPanel = panelRef.resolve(); + + if (vizPanel.parent instanceof SceneGridItem || vizPanel.parent instanceof PanelRepeaterGridItem) { + const dashboardScene = dashboardRef.resolve(); + const panelJson = gridItemToPanel(vizPanel.parent); + const panelModel = new PanelModel(panelJson); + + const dashboardJson = transformSceneToSaveModel(dashboardScene); + const dashboardModel = new DashboardModel(dashboardJson); + + return ( + { + modalRef?.resolve().onDismiss(); + }} + /> + ); + } + + return null; +} diff --git a/public/app/features/dashboard-scene/sharing/ShareLinkTab.tsx b/public/app/features/dashboard-scene/sharing/ShareLinkTab.tsx index 61e38a55d65..113fc64d918 100644 --- a/public/app/features/dashboard-scene/sharing/ShareLinkTab.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareLinkTab.tsx @@ -30,6 +30,8 @@ interface ShareOptions { } export class ShareLinkTab extends SceneObjectBase { + public tabId = 'Link'; + static Component = ShareLinkTabRenderer; constructor(state: Omit) { diff --git a/public/app/features/dashboard-scene/sharing/ShareModal.tsx b/public/app/features/dashboard-scene/sharing/ShareModal.tsx index afda14680b4..c11599104cf 100644 --- a/public/app/features/dashboard-scene/sharing/ShareModal.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareModal.tsx @@ -10,6 +10,7 @@ import { DashboardScene } from '../scene/DashboardScene'; import { getDashboardSceneFor } from '../utils/utils'; import { ShareExportTab } from './ShareExportTab'; +import { ShareLibraryPanelTab } from './ShareLibraryPanelTab'; import { ShareLinkTab } from './ShareLinkTab'; import { SharePanelEmbedTab } from './SharePanelEmbedTab'; import { ShareSnapshotTab } from './ShareSnapshotTab'; @@ -51,34 +52,19 @@ export class ShareModal extends SceneObjectBase implements Moda tabs.push(new ShareSnapshotTab({ panelRef, dashboardRef, modalRef: this.getRef() })); } + if (panelRef) { + tabs.push(new SharePanelEmbedTab({ panelRef, dashboardRef })); + + if (panelRef.resolve() instanceof VizPanel) { + tabs.push(new ShareLibraryPanelTab({ panelRef, dashboardRef, modalRef: this.getRef() })); + } + } + if (Boolean(config.featureToggles['publicDashboards'])) { tabs.push(new SharePublicDashboardTab({ dashboardRef, modalRef: this.getRef() })); } - if (panelRef) { - tabs.push(new SharePanelEmbedTab({ panelRef, dashboardRef })); - } - this.setState({ tabs }); - - // if (panel) { - // const embedLabel = t('share-modal.tab-title.embed', 'Embed'); - // tabs.push({ label: embedLabel, value: shareDashboardType.embed, component: ShareEmbed }); - - // if (!isPanelModelLibraryPanel(panel)) { - // const libraryPanelLabel = t('share-modal.tab-title.library-panel', 'Library panel'); - // tabs.push({ label: libraryPanelLabel, value: shareDashboardType.libraryPanel, component: ShareLibraryPanel }); - // } - // tabs.push(...customPanelTabs); - // } else { - // const exportLabel = t('share-modal.tab-title.export', 'Export'); - // tabs.push({ - // label: exportLabel, - // value: shareDashboardType.export, - // component: ShareExport, - // }); - // tabs.push(...customDashboardTabs); - // } } onDismiss = () => { @@ -101,7 +87,7 @@ function SharePanelModalRenderer({ model }: SceneComponentProps) { const modalTabs = tabs?.map((tab) => ({ label: tab.getTabLabel(), - value: tab.getTabLabel(), + value: tab.tabId, })); const header = ( @@ -114,7 +100,7 @@ function SharePanelModalRenderer({ model }: SceneComponentProps) { /> ); - const currentTab = tabs.find((t) => t.getTabLabel() === activeTab); + const currentTab = tabs.find((t) => t.tabId === activeTab); return ( diff --git a/public/app/features/dashboard-scene/sharing/SharePanelEmbedTab.tsx b/public/app/features/dashboard-scene/sharing/SharePanelEmbedTab.tsx index 085ed4092bf..94d5d04667b 100644 --- a/public/app/features/dashboard-scene/sharing/SharePanelEmbedTab.tsx +++ b/public/app/features/dashboard-scene/sharing/SharePanelEmbedTab.tsx @@ -18,6 +18,7 @@ export interface SharePanelEmbedTabState extends SceneShareTabState { } export class SharePanelEmbedTab extends SceneObjectBase { + public tabId = 'Embed'; static Component = SharePanelEmbedTabRenderer; public constructor(state: SharePanelEmbedTabState) { diff --git a/public/app/features/dashboard-scene/sharing/ShareSnapshotTab.tsx b/public/app/features/dashboard-scene/sharing/ShareSnapshotTab.tsx index 2a1e79dd8f1..208fdd67b34 100644 --- a/public/app/features/dashboard-scene/sharing/ShareSnapshotTab.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareSnapshotTab.tsx @@ -50,6 +50,7 @@ export interface ShareSnapshotTabState extends SceneShareTabState { } export class ShareSnapshotTab extends SceneObjectBase { + public tabId = 'Snapshot'; static Component = ShareSnapshoTabRenderer; public constructor(state: ShareSnapshotTabState) { diff --git a/public/app/features/dashboard-scene/sharing/public-dashboards/SharePublicDashboardTab.tsx b/public/app/features/dashboard-scene/sharing/public-dashboards/SharePublicDashboardTab.tsx index e34123410cc..a7b9de9d741 100644 --- a/public/app/features/dashboard-scene/sharing/public-dashboards/SharePublicDashboardTab.tsx +++ b/public/app/features/dashboard-scene/sharing/public-dashboards/SharePublicDashboardTab.tsx @@ -17,6 +17,7 @@ export interface SharePublicDashboardTabState extends SceneShareTabState { } export class SharePublicDashboardTab extends SceneObjectBase { + public tabId = 'Public dashboard'; static Component = SharePublicDashboardTabRenderer; public getTabLabel() { diff --git a/public/app/features/dashboard-scene/sharing/types.ts b/public/app/features/dashboard-scene/sharing/types.ts index 3dafb3b89f2..dc4ddb676e6 100644 --- a/public/app/features/dashboard-scene/sharing/types.ts +++ b/public/app/features/dashboard-scene/sharing/types.ts @@ -10,4 +10,5 @@ export interface SceneShareTabState extends SceneObjectState { export interface SceneShareTab extends SceneObject { getTabLabel(): string; + tabId: string; } diff --git a/public/app/features/dashboard/components/ShareModal/ShareLibraryPanel.tsx b/public/app/features/dashboard/components/ShareModal/ShareLibraryPanel.tsx index e3b65aab155..b40dce681db 100644 --- a/public/app/features/dashboard/components/ShareModal/ShareLibraryPanel.tsx +++ b/public/app/features/dashboard/components/ShareModal/ShareLibraryPanel.tsx @@ -24,7 +24,7 @@ export const ShareLibraryPanel = ({ panel, initialFolderUid, onDismiss }: Props)

Create library panel.

- + ); }; diff --git a/public/app/features/library-panels/components/AddLibraryPanelModal/AddLibraryPanelModal.tsx b/public/app/features/library-panels/components/AddLibraryPanelModal/AddLibraryPanelModal.tsx index d4b9f8db378..73b086913c5 100644 --- a/public/app/features/library-panels/components/AddLibraryPanelModal/AddLibraryPanelModal.tsx +++ b/public/app/features/library-panels/components/AddLibraryPanelModal/AddLibraryPanelModal.tsx @@ -12,7 +12,7 @@ import { LibraryElementDTO } from '../../types'; import { usePanelSave } from '../../utils/usePanelSave'; interface AddLibraryPanelContentsProps { - onDismiss: () => void; + onDismiss?: () => void; panel: PanelModel; initialFolderUid?: string; } @@ -23,20 +23,23 @@ export const AddLibraryPanelContents = ({ panel, initialFolderUid, onDismiss }: const [debouncedPanelName, setDebouncedPanelName] = useState(panel.title); const [waiting, setWaiting] = useState(false); + console.log('folderUid', folderUid); useEffect(() => setWaiting(true), [panelName]); useDebounce(() => setDebouncedPanelName(panelName), 350, [panelName]); const { saveLibraryPanel } = usePanelSave(); + const onCreate = useCallback(() => { panel.libraryPanel = { uid: '', name: panelName }; saveLibraryPanel(panel, folderUid!).then((res: LibraryElementDTO | FetchError) => { if (!isFetchError(res)) { - onDismiss(); + onDismiss?.(); } else { panel.libraryPanel = undefined; } }); }, [panel, panelName, folderUid, onDismiss, saveLibraryPanel]); + const isValidName = useAsync(async () => { try { return !(await getLibraryPanelByName(panelName)).some((lp) => lp.folderUid === folderUid); @@ -50,6 +53,7 @@ export const AddLibraryPanelContents = ({ panel, initialFolderUid, onDismiss }: } }, [debouncedPanelName, folderUid]); + console.log('isValidName:', isValidName); const invalidInput = !isValidName?.value && isValidName.value !== undefined && panelName === debouncedPanelName && !waiting; diff --git a/yarn.lock b/yarn.lock index 8842cd5134d..75c0e48745b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3303,7 +3303,7 @@ __metadata: linkType: soft "@grafana/scenes@npm:1.24.1": - version: 1.24.1 + version: 0.0.0-use.local resolution: "@grafana/scenes@npm:1.24.1" dependencies: "@grafana/e2e-selectors": "npm:10.0.2" @@ -3318,7 +3318,7 @@ __metadata: "@grafana/ui": 10.0.3 checksum: 38967dd3977a9b9feb4c295da58bb378d2f9c810c43b34c67c65858782b9593421dfb58d29bc3fa0e86fc63f9af37f7a381ac958ef43bf38d6443a0a7e4de059 languageName: node - linkType: hard + linkType: soft "@grafana/schema@npm:10.3.0-pre, @grafana/schema@workspace:*, @grafana/schema@workspace:packages/grafana-schema": version: 0.0.0-use.local