diff --git a/e2e-playwright/dashboards-suite/dashboard-export-image.spec.ts b/e2e-playwright/dashboards-suite/dashboard-export-image.spec.ts new file mode 100644 index 00000000000..5a72d94f9d6 --- /dev/null +++ b/e2e-playwright/dashboards-suite/dashboard-export-image.spec.ts @@ -0,0 +1,52 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const DASHBOARD_UID = 'ZqZnVvFZz'; + +test.use({ + featureToggles: { + scenes: true, + newDashboardSharingComponent: true, + sharingDashboardImage: true, // Enable the export image feature + }, +}); + +test.describe( + 'Export as Image', + { + tag: ['@dashboards'], + }, + () => { + test('Show renderer not available message when plugin not installed', async ({ + gotoDashboardPage, + page, + selectors, + }) => { + // Navigate to a dashboard + const dashboardPage = await gotoDashboardPage({ + uid: DASHBOARD_UID, + }); + + // Open the export dropdown + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.NewExportButton.arrowMenu).click(); + + // Click export as image option + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.DashNav.NewExportButton.Menu.exportAsImage) + .click(); + + // Verify we're on the export image view + await expect(page).toHaveURL(/.*shareView=image/); + + // Verify the "renderer not available" alert is displayed + const rendererAlert = page.getByRole('status'); + await expect(rendererAlert).toBeVisible(); + await expect(rendererAlert).toContainText(/Image renderer plugin not installed/i); + await expect(rendererAlert).toContainText( + /To render an image, you must install the Grafana image renderer plugin/i + ); + + // Verify the generate button is NOT present when renderer is unavailable + await expect(page.getByRole('button', { name: /Generate image/i })).not.toBeVisible(); + }); + } +); diff --git a/public/app/features/dashboard-scene/sharing/ExportButton/ExportMenu.tsx b/public/app/features/dashboard-scene/sharing/ExportButton/ExportMenu.tsx index d5524108b07..c0099c17f2f 100644 --- a/public/app/features/dashboard-scene/sharing/ExportButton/ExportMenu.tsx +++ b/public/app/features/dashboard-scene/sharing/ExportButton/ExportMenu.tsx @@ -52,6 +52,7 @@ export default function ExportMenu({ dashboard }: { dashboard: DashboardScene }) menuItems.push({ shareId: shareDashboardType.image, + testId: newExportButtonSelector.exportAsImage, icon: 'camera', label: t('share-dashboard.menu.export-image-title', 'Export as image'), renderCondition: Boolean(config.featureToggles.sharingDashboardImage), diff --git a/public/app/features/dashboard-scene/sharing/ExportButton/utils.test.ts b/public/app/features/dashboard-scene/sharing/ExportButton/utils.test.ts index 1c4ad371d0a..fa13471055a 100644 --- a/public/app/features/dashboard-scene/sharing/ExportButton/utils.test.ts +++ b/public/app/features/dashboard-scene/sharing/ExportButton/utils.test.ts @@ -96,6 +96,13 @@ describe('Dashboard Export Image Utils', () => { const fetchMock = jest.fn().mockReturnValue(of({ ok: true, data: mockBlob })); (getBackendSrv as jest.Mock).mockReturnValue({ fetch: fetchMock }); + // Mock window.innerWidth + Object.defineProperty(window, 'innerWidth', { + writable: true, + configurable: true, + value: 1280, + }); + const dashboard = { state: { uid: 'test-uid', @@ -119,7 +126,7 @@ describe('Dashboard Export Image Utils', () => { absolute: true, updateQuery: { height: -1, - width: 1000, + width: 1280, scale: 2, kiosk: true, hideNav: true, @@ -128,5 +135,46 @@ describe('Dashboard Export Image Utils', () => { }, }); }); + + it('should fallback to config width when window.innerWidth is not available', async () => { + config.rendererAvailable = true; + config.rendererDefaultImageWidth = 1500; + const mockBlob = new Blob(['test'], { type: 'image/png' }); + const fetchMock = jest.fn().mockReturnValue(of({ ok: true, data: mockBlob })); + (getBackendSrv as jest.Mock).mockReturnValue({ fetch: fetchMock }); + + // Ensure window.innerWidth is undefined + Object.defineProperty(window, 'innerWidth', { + writable: true, + configurable: true, + value: undefined, + }); + + const dashboard = { + state: { + uid: 'test-uid', + }, + } as DashboardScene; + + const result = await generateDashboardImage({ dashboard, scale: 1 }); + + expect(result.error).toBeUndefined(); + expect(result.blob).toBe(mockBlob); + expect(getDashboardUrl).toHaveBeenCalledWith({ + uid: 'test-uid', + currentQueryParams: '', + render: true, + absolute: true, + updateQuery: { + height: -1, + width: 1500, // Should use config value + scale: 1, + kiosk: true, + hideNav: true, + orgId: '1', + fullPageImage: true, + }, + }); + }); }); }); diff --git a/public/app/features/dashboard-scene/sharing/ExportButton/utils.ts b/public/app/features/dashboard-scene/sharing/ExportButton/utils.ts index 64d84893b45..f9fb9e5f04b 100644 --- a/public/app/features/dashboard-scene/sharing/ExportButton/utils.ts +++ b/public/app/features/dashboard-scene/sharing/ExportButton/utils.ts @@ -28,7 +28,7 @@ export interface ImageGenerationResult { */ export async function generateDashboardImage({ dashboard, - scale = config.rendererDefaultImageScale || 1, + scale = config.rendererDefaultImageScale || 2, }: ImageGenerationOptions): Promise { try { // Check if renderer plugin is available @@ -46,7 +46,7 @@ export async function generateDashboardImage({ absolute: true, updateQuery: { height: -1, // image renderer will scroll through the dashboard and set the appropriate height - width: config.rendererDefaultImageWidth || 1000, + width: window.innerWidth || config.rendererDefaultImageWidth || 1000, scale, kiosk: true, hideNav: true,